yap-blog/src/actions/auth.ts
2026-07-02 21:32:33 -04:00

72 lines
2.3 KiB
TypeScript

"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<string> | null = null;
function dummyHash(): Promise<string> {
dummyHashPromise ??= hashPassword("dummy-password-for-timing");
return dummyHashPromise;
}
export async function loginAction(_prev: FormState, formData: FormData): Promise<FormState> {
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<void> {
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");
}