// WHY: Basic Auth has no native logout and its dialog cannot be styled, so a // successful /login form submit receives a server-side session token in an // HttpOnly cookie. Basic Auth remains valid in parallel (curl, API use). const crypto = require('crypto'); const SESSION_COOKIE = 'cms_session'; const SESSION_TTL_MS = 8 * 60 * 60 * 1000; const sessions = new Map(); // token -> expiresAt (ms) function timingSafeMatch(candidate, expected) { if (typeof candidate !== 'string' || typeof expected !== 'string') return false; const cHash = crypto.createHash('sha256').update(candidate).digest(); const eHash = crypto.createHash('sha256').update(expected).digest(); return crypto.timingSafeEqual(cHash, eHash); } function validateLogin(user, pass, expectedUser, expectedPass) { if (!expectedUser || !expectedPass) return false; const userOk = timingSafeMatch(user, expectedUser); const passOk = timingSafeMatch(pass, expectedPass); return Boolean(userOk && passOk); } function createSessionCookie(isSecure) { const token = crypto.randomBytes(32).toString('hex'); sessions.set(token, Date.now() + SESSION_TTL_MS); return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}${isSecure ? '; Secure' : ''}`; } function clearSessionCookie() { return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`; } function getSessionToken(req) { const cookies = req.headers.cookie || ''; const match = cookies.match(new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([a-f0-9]+)`)); return match ? match[1] : null; } function hasValidSession(req) { const token = getSessionToken(req); if (!token) return false; const expiresAt = sessions.get(token); if (!expiresAt) return false; if (Date.now() > expiresAt) { sessions.delete(token); return false; } return true; } function deleteSession(req) { const token = getSessionToken(req); if (token) sessions.delete(token); } module.exports = { SESSION_COOKIE, SESSION_TTL_MS, timingSafeMatch, validateLogin, createSessionCookie, clearSessionCookie, hasValidSession, deleteSession, };