TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { NextResponse, type NextRequest } from 'next/server';23/**4 * Request guard for the admin surface (§144). Runs before rendering: `/admin/*` and `/api/admin/*`5 * require the `ri_admin` cookie whose value is HMAC-SHA256(SESSION_SECRET, "admin:" + ADMIN_TOKEN).6 * Web Crypto is used because this runs on the edge-compatible runtime.7 */8const ADMIN_COOKIE = 'ri_admin';910async function expectedCookie(): Promise<string | null> {11 const token = process.env.ADMIN_TOKEN;12 if (!token) return null;13 const enc = new TextEncoder();14 const key = await crypto.subtle.importKey('raw', enc.encode(process.env.SESSION_SECRET ?? 'dev-only'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);15 const sig = await crypto.subtle.sign('HMAC', key, enc.encode(`admin:${token}`));16 return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, '0')).join('');17}1819export async function proxy(req: NextRequest) {20 const { pathname } = req.nextUrl;21 if (pathname === '/admin/login' || pathname === '/api/admin/login') return NextResponse.next();22 const expected = await expectedCookie();23 const got = req.cookies.get(ADMIN_COOKIE)?.value;24 const ok = Boolean(expected && got && got.length === expected.length && constantTimeEqual(got, expected));25 if (ok) return NextResponse.next();26 if (pathname.startsWith('/api/')) return NextResponse.json({ error: 'Admin authentication required' }, { status: 401 });27 const url = req.nextUrl.clone();28 url.pathname = '/admin/login';29 url.searchParams.set('next', pathname);30 return NextResponse.redirect(url);31}3233function constantTimeEqual(a: string, b: string): boolean {34 let out = 0;35 for (let i = 0; i < a.length; i++) out |= a.charCodeAt(i) ^ b.charCodeAt(i);36 return out === 0;37}3839export const config = {40 matcher: ['/admin/:path*', '/api/admin/:path*'],41};42