import rateLimit from 'express-rate-limit' import qrcode from 'qrcode' import { parseCookies } from '../helpers.js' import { APP_VERSION, GIT_COMMIT } from '../config.js' import { isAdminPasswordConfigured, isValidAdminSession, requireAdminAuth, setAdminSessionCookie, clearAdminSessionCookie, createAdminSession, deleteAdminSession, isAdminPasswordValid, isTotpEnabled, loadTotpState, saveTotpState, generateTotpSecret, getTotpUri, verifyTotpCode, generateRecoveryCodes, hashRecoveryCode, consumeRecoveryCode, createPendingSession, consumePendingSession, } from '../auth.js' const ADMIN_SESSION_COOKIE = 'vbn_admin_session' const loginRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, standardHeaders: true, legacyHeaders: false, message: { message: 'Too many login attempts. Please wait 15 minutes and try again.' }, skipSuccessfulRequests: true, }) export function register(app) { app.get('/api/admin-auth/status', async (req, res) => { res.json({ authenticated: isValidAdminSession(req), configured: isAdminPasswordConfigured(), totpEnabled: await isTotpEnabled(), version: APP_VERSION, commit: GIT_COMMIT, }) }) app.post('/api/admin-auth/login', loginRateLimiter, async (req, res) => { const password = typeof req.body?.password === 'string' ? req.body.password : '' if (!isAdminPasswordConfigured()) { res.status(503).json({ message: 'ADMIN_PASSWORD is not configured on the server.' }) return } if (!isAdminPasswordValid(password)) { res.status(401).json({ message: 'Invalid password.' }) return } const totpOn = await isTotpEnabled() if (totpOn) { const pendingToken = createPendingSession() res.json({ totpRequired: true, pendingToken }) return } const sessionToken = createAdminSession() setAdminSessionCookie(res, sessionToken) res.json({ ok: true }) }) app.post('/api/admin-auth/totp-verify', loginRateLimiter, async (req, res) => { const { pendingToken, code } = req.body ?? {} if (!consumePendingSession(pendingToken)) { res.status(401).json({ message: 'Session expired or invalid. Please sign in again.' }) return } const totpState = await loadTotpState() if (!totpState?.secret || !totpState?.verified) { res.status(400).json({ message: 'TOTP is not configured.' }) return } const codeStr = typeof code === 'string' ? code.trim() : '' if (verifyTotpCode(totpState.secret, codeStr)) { const sessionToken = createAdminSession() setAdminSessionCookie(res, sessionToken) res.json({ ok: true }) return } if (consumeRecoveryCode(totpState, codeStr)) { await saveTotpState(totpState) const sessionToken = createAdminSession() setAdminSessionCookie(res, sessionToken) res.json({ ok: true, usedRecoveryCode: true, remainingRecoveryCodes: totpState.hashedRecoveryCodes.length }) return } res.status(401).json({ message: 'Invalid code. Try again or use a recovery code.' }) }) app.post('/api/admin-auth/totp-setup-init', requireAdminAuth, async (req, res) => { const secret = generateTotpSecret() const uri = getTotpUri(secret) const qrDataUrl = await qrcode.toDataURL(uri) const existing = await loadTotpState() await saveTotpState({ ...existing, secret, verified: false }) res.json({ qrDataUrl, secret }) }) app.post('/api/admin-auth/totp-setup-confirm', requireAdminAuth, async (req, res) => { const { code } = req.body ?? {} const totpState = await loadTotpState() if (!totpState?.secret) { res.status(400).json({ message: 'No TOTP setup in progress. Call /totp-setup-init first.' }) return } if (!verifyTotpCode(totpState.secret, typeof code === 'string' ? code.trim() : '')) { res.status(401).json({ message: 'Code incorrect. Scan the QR code again and try once more.' }) return } const recoveryCodes = generateRecoveryCodes() await saveTotpState({ secret: totpState.secret, verified: true, hashedRecoveryCodes: recoveryCodes.map(hashRecoveryCode), enabledAt: new Date().toISOString(), }) res.json({ ok: true, recoveryCodes }) }) app.post('/api/admin-auth/totp-disable', requireAdminAuth, async (req, res) => { await saveTotpState({ secret: null, verified: false, hashedRecoveryCodes: [], disabledAt: new Date().toISOString() }) res.json({ ok: true }) }) app.post('/api/admin-auth/totp-regen-recovery', requireAdminAuth, async (req, res) => { const totpState = await loadTotpState() if (!totpState?.secret || !totpState?.verified) { res.status(400).json({ message: 'TOTP is not enabled.' }) return } const recoveryCodes = generateRecoveryCodes() await saveTotpState({ ...totpState, hashedRecoveryCodes: recoveryCodes.map(hashRecoveryCode) }) res.json({ ok: true, recoveryCodes }) }) app.post('/api/admin-auth/logout', (req, res) => { const cookies = parseCookies(req.headers.cookie) const sessionToken = cookies[ADMIN_SESSION_COOKIE] deleteAdminSession(sessionToken) clearAdminSessionCookie(res) res.json({ ok: true }) }) }