SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
2.4 KB · 60 lines typescript
Raw Blame History
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