import { createHmac, timingSafeEqual } from "crypto"; import { cookies } from "next/headers"; export const ADMIN_SESSION_COOKIE = "moh_admin_session"; const ADMIN_FAIL_COOKIE = "moh_admin_fail"; const ADMIN_SESSION_VALUE = "superadmin"; const MAX_FAILED_ATTEMPTS = 5; const LOCKOUT_SECONDS = 15 * 60; function getSecret(): string { return process.env.ADMIN_AUTH_SECRET ?? ""; } function getPassword(): string { return process.env.ADMIN_PASSWORD ?? ""; } function signValue(value: string): string { return createHmac("sha256", getSecret()).update(value).digest("hex"); } function buildToken(): string { return `${ADMIN_SESSION_VALUE}.${signValue(ADMIN_SESSION_VALUE)}`; } function verifyToken(token: string): boolean { const parts = token.split("."); if (parts.length !== 2) { return false; } const [value, signature] = parts; if (value !== ADMIN_SESSION_VALUE) { return false; } const expected = signValue(value); const left = Buffer.from(signature); const right = Buffer.from(expected); if (left.length !== right.length) { return false; } return timingSafeEqual(left, right); } export function isAdminAuthConfigured(): boolean { return getPassword().length > 0 && getSecret().length > 0; } export function isPasswordValid(password: string): boolean { if (!isAdminAuthConfigured()) { return false; } const provided = Buffer.from(password); const expected = Buffer.from(getPassword()); if (provided.length !== expected.length) { return false; } return timingSafeEqual(provided, expected); } export function setAdminSessionCookie(): void { const store = cookies(); store.set(ADMIN_SESSION_COOKIE, buildToken(), { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: 60 * 60 * 8, }); } export function clearAdminSessionCookie(): void { const store = cookies(); store.set(ADMIN_SESSION_COOKIE, "", { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: 0, }); } type FailState = { attempts: number; lockUntil: number; }; function parseFailState(rawValue: string | undefined): FailState { if (!rawValue) { return { attempts: 0, lockUntil: 0 }; } try { const parsed = JSON.parse(rawValue) as Partial; return { attempts: Number(parsed.attempts ?? 0), lockUntil: Number(parsed.lockUntil ?? 0), }; } catch { return { attempts: 0, lockUntil: 0 }; } } export function getAdminLockState(): { locked: boolean; remainingSeconds: number } { const store = cookies(); const state = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value); const now = Date.now(); if (state.lockUntil > now) { return { locked: true, remainingSeconds: Math.ceil((state.lockUntil - now) / 1000), }; } return { locked: false, remainingSeconds: 0 }; } export function registerFailedAdminAttempt(): { locked: boolean; remainingSeconds: number } { const store = cookies(); const now = Date.now(); const current = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value); const attempts = current.lockUntil > now ? current.attempts : current.attempts + 1; const locked = attempts >= MAX_FAILED_ATTEMPTS; const lockUntil = locked ? now + LOCKOUT_SECONDS * 1000 : 0; store.set(ADMIN_FAIL_COOKIE, JSON.stringify({ attempts, lockUntil }), { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: LOCKOUT_SECONDS, }); return { locked, remainingSeconds: locked ? LOCKOUT_SECONDS : 0, }; } export function resetAdminFailedAttempts(): void { const store = cookies(); store.set(ADMIN_FAIL_COOKIE, "", { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: 0, }); } export function isAdminAuthenticated(): boolean { if (!isAdminAuthConfigured()) { return false; } const store = cookies(); const token = store.get(ADMIN_SESSION_COOKIE)?.value; if (!token) { return false; } return verifyToken(token); }