SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
1.7 KB · 50 lines typescript
Raw Blame History
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