import { randomUUID } from 'crypto'; import { NextFunction, Request, Response } from 'express'; // Single-user session-token auth (no OAuth, per tech-stack.md). // // A single shared password is read from the AUTH_PASSWORD env var. If it's // unset/empty, auth is DISABLED — every request passes through — which keeps // local dev frictionless. When set, clients must POST it to /api/auth/login to // receive an httpOnly session cookie, checked on every protected route. // // Tokens live in memory only: a server restart invalidates all sessions // (you log in again). That's an acceptable trade for a single-user, self-hosted // app and avoids persisting a session table. const COOKIE_NAME = 'mkb_session'; const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days const password = process.env.AUTH_PASSWORD ?? ''; export const authEnabled = password.length > 0; const sessions = new Set(); export function logAuthStatus() { if (authEnabled) { console.log('Auth: enabled (AUTH_PASSWORD is set).'); } else { console.warn( 'Auth: DISABLED — AUTH_PASSWORD is not set, so the API is open to anyone ' + 'who can reach it. Set AUTH_PASSWORD to require a login.' ); } } function parseCookies(header: string | undefined): Record { const out: Record = {}; if (!header) return out; for (const part of header.split(';')) { const eq = part.indexOf('='); if (eq === -1) continue; const key = part.slice(0, eq).trim(); if (key) out[key] = decodeURIComponent(part.slice(eq + 1).trim()); } return out; } function readSessionToken(req: Request): string | undefined { return parseCookies(req.headers.cookie)[COOKIE_NAME]; } export function isAuthenticated(req: Request): boolean { if (!authEnabled) return true; const token = readSessionToken(req); return token != null && sessions.has(token); } export function requireAuth(req: Request, res: Response, next: NextFunction) { if (isAuthenticated(req)) { next(); return; } res.status(401).json({ error: 'Unauthorized' }); } export function login(res: Response, attempt: unknown): boolean { if (!authEnabled) return true; if (typeof attempt !== 'string' || attempt !== password) return false; const token = randomUUID(); sessions.add(token); res.cookie(COOKIE_NAME, token, { httpOnly: true, sameSite: 'lax', path: '/', maxAge: SESSION_TTL_MS, }); return true; } export function logout(req: Request, res: Response) { const token = readSessionToken(req); if (token) sessions.delete(token); res.clearCookie(COOKIE_NAME, { path: '/' }); }