import 'server-only'; import { createHmac, timingSafeEqual } from 'node:crypto'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; export const ADMIN_COOKIE = 'ri_admin'; /** Cookie value = HMAC(SESSION_SECRET, ADMIN_TOKEN): the token itself never travels after login. */ export function adminCookieValue(): string | null { const token = process.env.ADMIN_TOKEN; if (!token) return null; return createHmac('sha256', process.env.SESSION_SECRET ?? 'dev-only').update(`admin:${token}`).digest('hex'); } export function isValidAdminCookie(value: string | undefined | null): boolean { const expected = adminCookieValue(); if (!expected || !value || value.length !== expected.length) return false; return timingSafeEqual(Buffer.from(value), Buffer.from(expected)); } export function tokenMatches(token: string): boolean { const expected = process.env.ADMIN_TOKEN; if (!expected || token.length !== expected.length) return false; return timingSafeEqual(Buffer.from(token), Buffer.from(expected)); } export async function isAdmin(): Promise { const jar = await cookies(); return isValidAdminCookie(jar.get(ADMIN_COOKIE)?.value); } /** Server-component guard (the proxy already blocks unauthenticated requests; this is defence in depth). */ export async function requireAdmin(): Promise { if (!(await isAdmin())) redirect('/admin/login'); }