55 lines
1.4 KiB
TypeScript
55 lines
1.4 KiB
TypeScript
"use server";
|
|
|
|
import { env } from "@/env";
|
|
import { checkLogin } from "@/lib/auth/password";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
|
|
|
export type PrecheckResult = "ok" | "invalid" | "twofactor";
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export async function precheckLogin(
|
|
username: string,
|
|
password: string,
|
|
): Promise<PrecheckResult> {
|
|
const u = String(username ?? "")
|
|
.normalize("NFC")
|
|
.trim();
|
|
const p = String(password ?? "");
|
|
if (!u || !p) return "invalid";
|
|
|
|
if (!(await rateLimit(`precheck:${await clientIp()}`, 10, 5 * 60_000)).ok)
|
|
return "invalid";
|
|
|
|
let user: { password: string; twoFactorConfirmedAt: Date | null } | null;
|
|
try {
|
|
user = await prisma.user.findUnique({
|
|
where: { username: u },
|
|
select: { password: true, twoFactorConfirmedAt: true },
|
|
});
|
|
} 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";
|
|
|
|
return user.twoFactorConfirmedAt ? "twofactor" : "ok";
|
|
}
|