spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { cookies } from 'next/headers';4import { redirect } from 'next/navigation';5import { ADMIN_COOKIE, adminConfigured, cookieValueFor, isAdmin, tokenMatches } from '@/lib/admin/auth';67export const metadata: Metadata = { title: 'Admin', robots: { index: false, follow: false } };8export const dynamic = 'force-dynamic';910const ADMIN_NAV = [11 { href: '/admin', label: 'Overview' },12 { href: '/admin/connectors', label: 'Connectors' },13 { href: '/admin/unresolved', label: 'Unresolved labels' },14 { href: '/admin/trace', label: 'Trace' },15 { href: '/admin/rankings', label: 'Rankings' },16];1718/**19 * Admin console (§140-141, §253). Token via `?token=` once → httpOnly cookie (sha256 of the token)20 * compared to ADMIN_TOKEN. Wrong or missing token renders a login hint instead of the console.21 */22export default async function AdminLayout({ children }: { children: React.ReactNode }) {23 if (!adminConfigured()) {24 return (25 <div className="py-10">26 <p className="ci-kicker">Admin</p>27 <h1 className="text-3xl">Admin console disabled</h1>28 <p className="mt-2 text-[14px] text-ink-2">Set ADMIN_TOKEN in the environment to enable the console.</p>29 </div>30 );31 }32 // Token exchange (`?token=` → cookie + redirect) is handled by src/proxy.ts before this renders.33 const authed = await isAdmin();34 if (!authed) {35 return (36 <div className="py-10">37 <p className="ci-kicker">Admin</p>38 <h1 className="text-3xl">Sign in</h1>39 <p className="mt-2 max-w-xl text-[14px] text-ink-2">Append <code className="ci-mono">?token=<ADMIN_TOKEN></code> to an admin URL once; a session cookie (hash of the token, httpOnly) is then set and the token is removed from the URL.</p>40 <form action={login} className="mt-4 flex max-w-md gap-2">41 <input name="token" type="password" placeholder="ADMIN_TOKEN" className="flex-1 border border-rule-strong bg-white px-3 py-1.5 text-[14px] outline-none focus:border-accent" aria-label="Admin token" />42 <button type="submit" className="border border-ink bg-ink px-3 py-1.5 text-[14px] text-paper hover:bg-ink-2">43 Sign in44 </button>45 </form>46 </div>47 );48 }49 return (50 <div className="pt-6">51 <div className="flex flex-wrap items-baseline justify-between gap-3 border-b border-rule pb-2">52 <p className="ci-kicker">Admin console · not indexed</p>53 <nav aria-label="Admin" className="flex flex-wrap gap-4 text-[13.5px]">54 {ADMIN_NAV.map((n) => (55 <Link key={n.href} href={n.href} className="text-ink-2 no-underline hover:text-accent">56 {n.label}57 </Link>58 ))}59 <form action={logout}>60 <button type="submit" className="text-ink-3 hover:text-danger">61 Sign out62 </button>63 </form>64 </nav>65 </div>66 {children}67 </div>68 );69}7071async function login(formData: FormData) {72 'use server';73 const token = String(formData.get('token') ?? '');74 if (tokenMatches(token)) {75 (await cookies()).set(ADMIN_COOKIE, cookieValueFor(token), { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', path: '/', maxAge: 60 * 60 * 12 });76 }77 redirect('/admin');78}7980async function logout() {81 'use server';82 (await cookies()).delete(ADMIN_COOKIE);83 redirect('/admin');84}85