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.
57 lines
1.5 KiB
TypeScript
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";
|
|
}
|