import "server-only"; import { adminFilePath, createAdminRecord, hashPassword, readAdminFile, verifyPassword, writeAdminFile, type AdminFile, } from "./admin-file"; /** * The app's view of the admin login. The file is re-read on each call (it's tiny), so an * admin created or reset with the CLI takes effect without restarting the server. */ export function getAdmin(): Promise { return readAdminFile(adminFilePath()); } export async function hasAdmin(): Promise { return (await getAdmin()) !== null; } // Hashed once, lazily: lets a failed login cost the same whether or not the username exists. let dummyHash: Promise | null = null; /** * True only for the admin's exact username and password. Always spends one full scrypt * hash, so timing doesn't reveal whether an admin exists or the username was right. */ export async function checkLogin(username: string, password: string): Promise { const admin = await getAdmin(); const userMatches = admin !== null && admin.username === username; const hash = userMatches ? admin.passwordHash : await (dummyHash ??= hashPassword("not the password")); const passwordMatches = await verifyPassword(password, hash); return userMatches && passwordMatches; } /** Creates the first admin. Fails with EEXIST if one already exists. */ export async function createAdmin(username: string, password: string): Promise { const admin = await createAdminRecord(username, password); await writeAdminFile(adminFilePath(), admin); return admin; }