spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import 'server-only';2import { createHash, timingSafeEqual } from 'node:crypto';3import { cookies } from 'next/headers';45/**6 * Admin gate. The browser never sees the token: the login form posts it to a server action which compares it7 * (constant time) with `process.env.CA_ADMIN_TOKEN` and sets an httpOnly cookie holding a SHA-256 of the8 * token. Pages check the cookie (again constant time) and server actions call the API with the real token.9 */10export const ADMIN_COOKIE = 'ca_admin';11export const ADMIN_COOKIE_MAX_AGE = 12 * 3600;1213export function adminToken(): string | null {14 const v = process.env.CA_ADMIN_TOKEN?.trim();15 return v ? v : null;16}1718export function isAdminConfigured(): boolean {19 return adminToken() !== null;20}2122function sha256(s: string): Buffer {23 return createHash('sha256').update(s, 'utf8').digest();24}2526/** Constant-time comparison of two strings (hashed first so lengths never leak). */27export function safeEqual(a: string, b: string): boolean {28 return timingSafeEqual(sha256(a), sha256(b));29}3031/** Cookie value = sha256(token) hex, salted with a fixed context string so it is never equal to the token. */32export function cookieValueFor(token: string): string {33 return createHash('sha256').update(`countryatlas-admin:${token}`, 'utf8').digest('hex');34}3536export function verifyToken(input: string): boolean {37 const expected = adminToken();38 if (!expected) return false;39 return safeEqual(input, expected);40}4142export async function isAdminAuthed(): Promise<boolean> {43 const expected = adminToken();44 if (!expected) return false;45 const store = await cookies();46 const c = store.get(ADMIN_COOKIE)?.value;47 if (!c) return false;48 return safeEqual(c, cookieValueFor(expected));49}50