Files
EpicNext-Cms/src/actions/auth-precheck.ts
T
openhands 89c1751e79
CI / check (push) Successful in 35s
CI / release (push) Skipped
CI / deploy (push) Successful in 58s
refactor: extract shared login credential verification into auth/login-core
The username normalization, dummy-hash constant, password check and
email-verification gate were duplicated between precheckLogin and the
NextAuth credentials authorize handler. Move them into a single
login-core module so both paths share one source of truth and stay
consistent.
2026-08-27 15:17:54 +02:00

57 lines
1.5 KiB
TypeScript

"use server";
import {
getLoginUser,
isEmailUnverified,
normalizeLoginInput,
runDummyHashCheck,
verifyLoginPassword,
} from "@/lib/auth/login-core";
import { clientIp, rateLimit } from "@/lib/rate-limit";
import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha";
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 { username: u, password: p } = normalizeLoginInput(username, 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";
}
const user = await getLoginUser(u);
if (!user) {
// Prevent timing-based enumeration: always run a dummy hash check.
await runDummyHashCheck(p);
return "invalid";
}
const res = await verifyLoginPassword(user, p);
if (!res.valid) return "invalid";
if (await isEmailUnverified(user)) {
return "unverified";
}
return user.twoFactorConfirmedAt ? "twofactor" : "ok";
}