Files
EpicNext-Cms/src/actions/auth-precheck.ts
T

90 lines
2.2 KiB
TypeScript

"use server";
import { eq } from "drizzle-orm";
import { env } from "@/env";
import { checkLogin } from "@/lib/auth/password";
import { db, User } 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 {
const [row] = await db
.select({
password: User.password,
twoFactorConfirmedAt: User.twoFactorConfirmedAt,
mail: User.mail,
mailVerified: User.mailVerified,
})
.from(User)
.where(eq(User.username, u))
.limit(1);
user = row ?? null;
} 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";
}