import { NextResponse, type NextRequest } from 'next/server'; /** * Request guard for the admin surface (ยง144). Runs before rendering: `/admin/*` and `/api/admin/*` * require the `ri_admin` cookie whose value is HMAC-SHA256(SESSION_SECRET, "admin:" + ADMIN_TOKEN). * Web Crypto is used because this runs on the edge-compatible runtime. */ const ADMIN_COOKIE = 'ri_admin'; async function expectedCookie(): Promise { const token = process.env.ADMIN_TOKEN; if (!token) return null; const enc = new TextEncoder(); const key = await crypto.subtle.importKey('raw', enc.encode(process.env.SESSION_SECRET ?? 'dev-only'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, enc.encode(`admin:${token}`)); return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, '0')).join(''); } export async function proxy(req: NextRequest) { const { pathname } = req.nextUrl; if (pathname === '/admin/login' || pathname === '/api/admin/login') return NextResponse.next(); const expected = await expectedCookie(); const got = req.cookies.get(ADMIN_COOKIE)?.value; const ok = Boolean(expected && got && got.length === expected.length && constantTimeEqual(got, expected)); if (ok) return NextResponse.next(); if (pathname.startsWith('/api/')) return NextResponse.json({ error: 'Admin authentication required' }, { status: 401 }); const url = req.nextUrl.clone(); url.pathname = '/admin/login'; url.searchParams.set('next', pathname); return NextResponse.redirect(url); } function constantTimeEqual(a: string, b: string): boolean { let out = 0; for (let i = 0; i < a.length; i++) out |= a.charCodeAt(i) ^ b.charCodeAt(i); return out === 0; } export const config = { matcher: ['/admin/:path*', '/api/admin/:path*'], };