import type { Metadata } from 'next'; import Link from 'next/link'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import { ADMIN_COOKIE, adminConfigured, cookieValueFor, isAdmin, tokenMatches } from '@/lib/admin/auth'; export const metadata: Metadata = { title: 'Admin', robots: { index: false, follow: false } }; export const dynamic = 'force-dynamic'; const ADMIN_NAV = [ { href: '/admin', label: 'Overview' }, { href: '/admin/connectors', label: 'Connectors' }, { href: '/admin/unresolved', label: 'Unresolved labels' }, { href: '/admin/trace', label: 'Trace' }, { href: '/admin/rankings', label: 'Rankings' }, ]; /** * Admin console (§140-141, §253). Token via `?token=` once → httpOnly cookie (sha256 of the token) * compared to ADMIN_TOKEN. Wrong or missing token renders a login hint instead of the console. */ export default async function AdminLayout({ children }: { children: React.ReactNode }) { if (!adminConfigured()) { return (

Admin

Admin console disabled

Set ADMIN_TOKEN in the environment to enable the console.

); } // Token exchange (`?token=` → cookie + redirect) is handled by src/proxy.ts before this renders. const authed = await isAdmin(); if (!authed) { return (

Admin

Sign in

Append ?token=<ADMIN_TOKEN> to an admin URL once; a session cookie (hash of the token, httpOnly) is then set and the token is removed from the URL.

); } return (

Admin console · not indexed

{children}
); } async function login(formData: FormData) { 'use server'; const token = String(formData.get('token') ?? ''); if (tokenMatches(token)) { (await cookies()).set(ADMIN_COOKIE, cookieValueFor(token), { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', path: '/', maxAge: 60 * 60 * 12 }); } redirect('/admin'); } async function logout() { 'use server'; (await cookies()).delete(ADMIN_COOKIE); redirect('/admin'); }