Database: - Add missing indexes (users.credits, users_currency(type,amount), users_settings.respects_received, camera_web.timestamp, messenger_offline.user_id) via migrations 0020/0021 - Use partial .select() everywhere instead of SELECT * (tickets, users, rooms, audit logs, catalog tree, polls, radio, password reset) - Add queryPrepared/queryPreparedOne (server-side prepared statements) and switch the login check to a prepared statement; drop dead cache options from the pool config - Raise total_users/total_rooms COUNT(*) cache TTL to 5m Caching: - Consolidate the three cache helpers (cached, redisCache, cachedQuery) into a single memory-first implementation backed by Redis - invalidateKey now clears the in-process cache as well as Redis - Cache homepage sections, news list, and leaderboard tabs; share one news_list cache key between homepage and news archive - siteSettings: in-process cache with TTL so repeated getters no longer pay a Redis round-trip per call - Share a 10s poll cache across all radio SSE connections - Normalize timestamps after cache reads (Redis JSON round-trip) Assets: - Enable AVIF/WebP via images.formats and remove unoptimized from news covers and the homepage hero (149KB jpg) with proper sizes/priority - Support ?format=webp|avif|png in the /imaging proxy via sharp Other: - Fix pnpm supply-chain minimumReleaseAge failures by excluding the freshly-published packages (next 16.3.1, hookform resolvers 5.8.0, resend 6.20.0) - Remove unused before/after fields from housekeeping AuditEntry
89 lines
2.2 KiB
TypeScript
89 lines
2.2 KiB
TypeScript
"use server";
|
|
|
|
import { env } from "@/env";
|
|
import { checkLogin } from "@/lib/auth/password";
|
|
import { queryPreparedOne } from "@/lib/db";
|
|
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
|
import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha";
|
|
import { siteSettings } from "@/lib/services/site-settings";
|
|
|
|
export type PrecheckResult =
|
|
| "ok"
|
|
| "invalid"
|
|
| "twofactor"
|
|
| "unverified"
|
|
| "captcha";
|
|
|
|
/**
|
|
* Validates username+password WITHOUT creating a session, and reports whether a
|
|
* TOTP code is still required. Lets the login form do the two-step 2FA flow.
|
|
* Also enforces captcha + optional email-verification when configured.
|
|
*/
|
|
export async function precheckLogin(
|
|
username: string,
|
|
password: string,
|
|
captchaToken?: string | null,
|
|
): Promise<PrecheckResult> {
|
|
const u = String(username ?? "")
|
|
.normalize("NFC")
|
|
.trim();
|
|
const p = String(password ?? "");
|
|
if (!u || !p) return "invalid";
|
|
|
|
const ip = await clientIp();
|
|
if (!(await rateLimit(`precheck:${ip}`, 10, 5 * 60_000)).ok) return "invalid";
|
|
|
|
const cfg = await captchaConfig();
|
|
if (cfg.provider !== "none") {
|
|
if (!(await verifyCaptcha(captchaToken ?? null, ip))) return "captcha";
|
|
}
|
|
|
|
let user: {
|
|
password: string;
|
|
twoFactorConfirmedAt: Date | null;
|
|
mail: string | null;
|
|
mailVerified: string;
|
|
} | null;
|
|
try {
|
|
user = await queryPreparedOne<{
|
|
password: string;
|
|
twoFactorConfirmedAt: Date | null;
|
|
mail: string | null;
|
|
mailVerified: string;
|
|
}>(
|
|
`SELECT password, two_factor_confirmed_at AS twoFactorConfirmedAt,
|
|
mail, mail_verified AS mailVerified
|
|
FROM users WHERE username = ? LIMIT 1`,
|
|
[u],
|
|
);
|
|
} catch {
|
|
return "invalid";
|
|
}
|
|
if (!user) {
|
|
// Prevent timing-based enumeration: always run a dummy hash check.
|
|
await checkLogin(
|
|
p,
|
|
"$2y$12$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcd",
|
|
{
|
|
convertPasswords: false,
|
|
},
|
|
);
|
|
return "invalid";
|
|
}
|
|
|
|
const res = await checkLogin(p, user.password, {
|
|
convertPasswords: env.CONVERT_PASSWORDS,
|
|
});
|
|
if (!res.valid) return "invalid";
|
|
|
|
if (
|
|
(await siteSettings.getBool("require_email_verification", false)) &&
|
|
user.mail &&
|
|
user.mailVerified !== "1"
|
|
) {
|
|
return "unverified";
|
|
}
|
|
|
|
return user.twoFactorConfirmedAt ? "twofactor" : "ok";
|
|
}
|