"use server"; import { checkLogin } from "@/lib/auth/password"; import { prisma } from "@/lib/prisma"; import { clientIp, rateLimit } from "@/lib/rate-limit"; import { env } from "@/env"; 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 { const u = String(username ?? "").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"; }