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 parseHostname(value: string | undefined): string | undefined { if (!value) { return undefined; } const trimmed = value.trim().toLowerCase(); if (!trimmed) { return undefined; } try { return new URL(trimmed).hostname.toLowerCase(); } catch { return trimmed.replace(/^https?:\/\//, "").split("/")[0]?.replace(/:\d+$/, "") || undefined; } } function isCookieDomainCandidate(hostname: string | undefined): hostname is string { return Boolean( hostname && hostname !== "localhost" && !hostname.endsWith(".localhost") && !/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && !hostname.includes(":"), ); } function getSharedCookieHostname(hostnames: string[]): string | undefined { const validHostnames = hostnames.filter(isCookieDomainCandidate); if (validHostnames.length === 0) { return undefined; } if (validHostnames.length === 1) { return validHostnames[0]; } const reversedSegments = validHostnames.map((hostname) => hostname.split(".").reverse()); const sharedSegments: string[] = []; for (let index = 0; index < reversedSegments[0].length; index += 1) { const segment = reversedSegments[0][index]; if (!segment || reversedSegments.some((parts) => parts[index] !== segment)) { break; } sharedSegments.push(segment); } if (sharedSegments.length < 2) { return validHostnames[0]; } return sharedSegments.reverse().join("."); } function getAdminCookieDomain(): string | undefined { const hostname = getSharedCookieHostname([ parseHostname(process.env.NEXT_PUBLIC_SITE_URL), parseHostname(process.env.NEXT_PUBLIC_ADMIN_URL), parseHostname(process.env.ADMIN_HOST), ].filter((value): value is string => Boolean(value))); return hostname ? `.${hostname}` : undefined; } 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 async function setAdminSessionCookie(): Promise { const store = await cookies(); store.set(ADMIN_SESSION_COOKIE, buildToken(), { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", domain: getAdminCookieDomain(), maxAge: 60 * 60 * 8, }); } export async function clearAdminSessionCookie(): Promise { const store = await cookies(); store.set(ADMIN_SESSION_COOKIE, "", { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", domain: getAdminCookieDomain(), 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 async function getAdminLockState(): Promise<{ locked: boolean; remainingSeconds: number }> { const store = await 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 async function registerFailedAdminAttempt(): Promise<{ locked: boolean; remainingSeconds: number }> { const store = await 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 async function resetAdminFailedAttempts(): Promise { const store = await cookies(); store.set(ADMIN_FAIL_COOKIE, "", { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: 0, }); } export async function isAdminAuthenticated(): Promise { if (!isAdminAuthConfigured()) { return false; } const store = await cookies(); const token = store.get(ADMIN_SESSION_COOKIE)?.value; if (!token) { return false; } return verifyToken(token); }