"use server"; import { eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { db } from "@/db"; import { users } from "@/db/schema"; import { clearSessionCookie, readSessionCookie, setSessionCookie, } from "@/lib/auth/cookies"; import { hashPassword, verifyPassword } from "@/lib/auth/password"; import { createSession, deleteExpiredSessions, deleteSession } from "@/lib/auth/session"; import type { FormState } from "@/lib/forms"; import { zodErrorToFormState } from "@/lib/forms"; import { loginFormSchema } from "@/lib/validation"; const GENERIC_LOGIN_ERROR = "Invalid username or password."; // Verified against when the username doesn't exist, so both failure paths // cost one scrypt derivation (no username-probing timing signal). let dummyHashPromise: Promise | null = null; function dummyHash(): Promise { dummyHashPromise ??= hashPassword("dummy-password-for-timing"); return dummyHashPromise; } export async function loginAction(_prev: FormState, formData: FormData): Promise { const parsed = loginFormSchema.safeParse({ username: formData.get("username"), password: formData.get("password"), }); if (!parsed.success) return zodErrorToFormState(parsed.error); let ok = false; try { const [user] = await db .select() .from(users) .where(eq(users.username, parsed.data.username)) .limit(1); const storedHash = user?.passwordHash ?? (await dummyHash()); const passwordOk = await verifyPassword(storedHash, parsed.data.password); ok = passwordOk && user !== undefined; if (ok && user) { await deleteExpiredSessions(); const { token, expiresAt } = await createSession(user.id); await setSessionCookie(token, expiresAt); } } catch (error) { console.error("loginAction failed", error); return { formError: "Could not sign in right now. Please try again." }; } if (!ok) return { formError: GENERIC_LOGIN_ERROR }; redirect("/admin"); } export async function logoutAction(): Promise { try { const token = await readSessionCookie(); if (token) await deleteSession(token); } catch (error) { // Losing the DB row is not fatal — the cookie is cleared regardless. console.error("logoutAction failed", error); } await clearSessionCookie(); redirect("/admin/login"); }