spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1'use client';23import Link from 'next/link';4import { usePathname } from 'next/navigation';5import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';6import { Logo } from '@/components/chrome/Logo';7import { TimeToggle } from '@/components/chrome/TimeToggle';8import { AdminError, adminFetch, getAdminToken, setAdminToken } from '@/lib/admin-fetch';910const NAV: [string, string][] = [11 ['/admin', 'Overview'],12 ['/admin/targets', 'Targets'],13 ['/admin/probes', 'Probes'],14 ['/admin/config', 'Scoring config'],15 ['/admin/baselines', 'Baselines'],16 ['/admin/raw', 'Raw explorer'],17 ['/admin/incidents', 'Incidents review'],18 ['/admin/annotations', 'Annotations'],19 ['/admin/replay', 'Replay'],20 ['/admin/boost', 'Boost'],21];2223const AuthCtx = createContext<{ authed: boolean; logout: () => void }>({ authed: false, logout: () => {} });24export const useAdminAuth = () => useContext(AuthCtx);2526/** Token gate: the token lives in sessionStorage only and is validated against /api/admin/overview. */27export function AdminShell({ children }: { children: ReactNode }) {28 const [state, setState] = useState<'checking' | 'locked' | 'ok'>('checking');29 const [input, setInput] = useState('');30 const [err, setErr] = useState<string | null>(null);31 const path = usePathname();3233 const verify = useCallback(async (token: string) => {34 setAdminToken(token);35 try {36 await adminFetch('/overview');37 setState('ok');38 setErr(null);39 } catch (e) {40 setAdminToken('');41 setState('locked');42 setErr(e instanceof AdminError && e.status === 401 ? 'Invalid token.' : 'Admin API unreachable.');43 }44 }, []);4546 useEffect(() => {47 const t = getAdminToken();48 // Defer to a microtask so no state is set synchronously inside the effect body.49 void Promise.resolve().then(() => (t ? verify(t) : setState('locked')));50 }, [verify]);5152 const logout = useCallback(() => {53 setAdminToken('');54 setState('locked');55 }, []);5657 return (58 <AuthCtx.Provider value={{ authed: state === 'ok', logout }}>59 <div className="flex min-h-screen flex-col">60 <header className="border-b border-line">61 <div className="mx-auto flex h-[var(--header-h)] max-w-[1600px] items-center gap-4 px-3 sm:px-5">62 <Link href="/admin" className="flex items-center gap-2 text-[14px] font-medium tracking-tight text-ink">63 <Logo size={20} color="var(--warn)" />64 InternetPressure <span className="label text-warn">admin</span>65 </Link>66 <div className="ml-auto flex items-center gap-3">67 <TimeToggle className="hidden sm:inline-flex" />68 <Link href="/" className="hidden text-[11.5px] text-ink-2 hover:text-ink sm:inline">69 public site →70 </Link>71 {state === 'ok' && (72 <button type="button" onClick={logout} className="rounded-[3px] border border-line px-2 py-1 text-[11px] text-ink-2 hover:text-ink">73 lock74 </button>75 )}76 </div>77 </div>78 {state === 'ok' && (79 <nav aria-label="Admin" className="scroll-x mx-auto flex max-w-[1600px] gap-1 px-3 sm:px-5">80 {NAV.map(([href, label]) => {81 const active = href === '/admin' ? path === '/admin' : path.startsWith(href);82 return (83 <Link key={href} href={href} aria-current={active ? 'page' : undefined} className={`whitespace-nowrap px-2 py-2 text-[11.5px] tracking-[0.04em] ${active ? 'text-ink' : 'text-ink-2 hover:text-ink'}`} style={active ? { boxShadow: 'inset 0 -1px 0 var(--warn)' } : undefined}>84 {label}85 </Link>86 );87 })}88 </nav>89 )}90 </header>91 <main className="mx-auto w-full max-w-[1600px] flex-1 px-3 py-5 sm:px-5">92 {state === 'checking' && <p className="text-[12px] text-ink-3">Checking token…</p>}93 {state === 'locked' && (94 <form95 className="mx-auto mt-[12vh] max-w-[380px]"96 onSubmit={(e) => {97 e.preventDefault();98 if (input.trim()) void verify(input.trim());99 }}100 >101 <p className="label">Restricted</p>102 <h1 className="mt-1 text-[22px] font-medium tracking-tight">Admin console</h1>103 <p className="mt-1 text-[12.5px] text-ink-2">Enter the admin token. It is kept in this tab's session storage only and sent as X-IP-Admin-Token.</p>104 <input type="password" value={input} onChange={(e) => setInput(e.target.value)} autoFocus autoComplete="off" placeholder="token" className="num mt-4 h-10 w-full rounded-[4px] border border-line bg-panel px-3 text-[14px] text-ink placeholder:text-ink-3" aria-label="Admin token" />105 {err && <p className="mt-2 text-[12px] text-bad">{err}</p>}106 <button type="submit" className="mt-3 h-9 w-full rounded-[4px] bg-warn text-[13px] font-medium text-bg hover:opacity-90">107 Unlock108 </button>109 </form>110 )}111 {state === 'ok' && children}112 </main>113 </div>114 </AuthCtx.Provider>115 );116}117