spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import 'server-only';2import { createHash, timingSafeEqual } from 'node:crypto';3import { cookies } from 'next/headers';4import { redirect } from 'next/navigation';56/**7 * Admin session: the operator posts the admin token once (`/api/admin/login`); we compare it server-side with8 * `SI_ADMIN_TOKEN` and set an httpOnly cookie holding sha256(token). Every admin request re-derives the expected hash9 * and compares in constant time. The raw token never reaches the browser (all FastAPI calls happen in `admin-api.ts`).10 */11export const ADMIN_COOKIE = 'si_admin';12export const ADMIN_SESSION_SECONDS = 12 * 3600;1314export function adminToken(): string {15 return process.env.SI_ADMIN_TOKEN ?? '';16}1718export function hashToken(token: string): string {19 return createHash('sha256').update(token, 'utf8').digest('hex');20}2122function safeEqual(a: string, b: string): boolean {23 const ba = Buffer.from(a, 'utf8');24 const bb = Buffer.from(b, 'utf8');25 return ba.length === bb.length && timingSafeEqual(ba, bb);26}2728/** True when `candidate` is the configured admin token (a blank server token disables the admin entirely). */29export function verifyToken(candidate: string | null | undefined): boolean {30 const expected = adminToken();31 if (!expected || !candidate) return false;32 return safeEqual(hashToken(candidate), hashToken(expected));33}3435export function verifyCookieValue(value: string | null | undefined): boolean {36 const expected = adminToken();37 if (!expected || !value) return false;38 return safeEqual(value, hashToken(expected));39}4041export async function isAdmin(): Promise<boolean> {42 const jar = await cookies();43 return verifyCookieValue(jar.get(ADMIN_COOKIE)?.value);44}4546/** Server components / route handlers: redirect anonymous visitors to the login form. */47export async function requireAdmin(next?: string): Promise<void> {48 if (!(await isAdmin())) redirect(next ? `/admin/login?next=${encodeURIComponent(next)}` : '/admin/login');49}5051export function cookieOptions(): { httpOnly: true; sameSite: 'lax'; secure: boolean; path: string; maxAge: number } {52 return { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', path: '/', maxAge: ADMIN_SESSION_SECONDS };53}5455/** Only allow same-site relative redirects after login. */56export function safeNextPath(next: string | null | undefined): string {57 if (!next || !next.startsWith('/admin') || next.startsWith('//')) return '/admin';58 return next;59}60