import crypto from 'crypto'; import config from '../config/config.js'; import logger from '../utils/logger.js'; const envSecret = process.env.ADMIN_SECRET; const configSecret = config.ADMIN_IDS?.[0]; if (!envSecret && !configSecret) { logger.fatal('ADMIN_SECRET environment variable is required. Set it before starting the admin panel.'); process.exit(1); } const TOKEN_SECRET = envSecret || configSecret; const COOKIE_NAME = 'admin_token'; const MAX_AGE = 24 * 60 * 60 * 1000; const SUPER_ADMIN_SECRET = process.env.SUPER_ADMIN_SECRET || undefined; const revokedTokens = new Set(); let globalLogoutTimestamp = 0; // Rate limiting for login const loginAttempts = new Map(); const MAX_ATTEMPTS = 5; const WINDOW_MS = 15 * 60 * 1000; function generateJti() { return crypto.randomBytes(16).toString('hex'); } function signToken(data) { const jti = generateJti(); const payload = JSON.stringify({ ...data, jti, iat: Date.now(), exp: Date.now() + MAX_AGE }); const b64 = Buffer.from(payload).toString('base64'); const sig = crypto.createHmac('sha256', TOKEN_SECRET).update(b64).digest('hex'); return { token: `${b64}.${sig}`, jti }; } function verifyToken(token) { try { const [b64, sig] = token.split('.'); const expected = crypto.createHmac('sha256', TOKEN_SECRET).update(b64).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null; const payload = JSON.parse(Buffer.from(b64, 'base64').toString()); if (payload.exp < Date.now()) return null; if (payload.iat < globalLogoutTimestamp) return null; if (revokedTokens.has(payload.jti)) return null; return payload; } catch { return null; } } export function requireAuth(req, res, next) { const token = req.cookies?.[COOKIE_NAME]; if (!token) return res.redirect('/login'); const payload = verifyToken(token); if (!payload) { res.clearCookie(COOKIE_NAME); return res.redirect('/login'); } req.admin = payload; res.locals.adminRole = payload.role || 'admin'; next(); } export function requireSuperAuth(req, res, next) { const token = req.cookies?.[COOKIE_NAME]; if (!token) { if (req.xhr || req.headers.accept?.includes('application/json')) { return res.status(401).json({ error: 'Authentication required' }); } return res.redirect('/login'); } const payload = verifyToken(token); if (!payload) { res.clearCookie(COOKIE_NAME); if (req.xhr || req.headers.accept?.includes('application/json')) { return res.status(401).json({ error: 'Invalid or expired token' }); } return res.redirect('/login'); } if (payload.role !== 'super_admin') { if (req.xhr || req.headers.accept?.includes('application/json')) { return res.status(403).json({ error: 'Super admin access required' }); } return res.status(403).send('Access denied. Super admin privileges required.'); } req.admin = payload; next(); } export function isSuperAdminWeb(req) { return req.admin && req.admin.role === 'super_admin'; } export function handleLogin(req, res) { const clientIp = req.ip || req.connection.remoteAddress; const now = Date.now(); const record = loginAttempts.get(clientIp); if (record && record.count >= MAX_ATTEMPTS && (now - record.firstAttempt) < WINDOW_MS) { return res.status(429).render('auth-login', { error: 'Too many attempts. Please try again in 15 minutes.', layout: false, pageTitle: 'Login' }); } const { token } = req.body || {}; const provided = Buffer.from(token || '', 'utf8'); const adminExpected = Buffer.from(TOKEN_SECRET, 'utf8'); const superAdminExpected = SUPER_ADMIN_SECRET ? Buffer.from(SUPER_ADMIN_SECRET, 'utf8') : null; const isAdminLogin = provided.length === adminExpected.length && crypto.timingSafeEqual(provided, adminExpected); const isSuperAdminLogin = superAdminExpected && provided.length === superAdminExpected.length && crypto.timingSafeEqual(provided, superAdminExpected); if (!isAdminLogin && !isSuperAdminLogin) { if (!record || (now - record.firstAttempt) >= WINDOW_MS) { loginAttempts.set(clientIp, { count: 1, firstAttempt: now }); } else { record.count += 1; } return res.status(401).render('auth-login', { error: 'Invalid token. Please try again.', layout: false, pageTitle: 'Login' }); } loginAttempts.delete(clientIp); const role = isSuperAdminLogin ? 'super_admin' : 'admin'; const { token: signed, jti } = signToken({ role }); res.cookie(COOKIE_NAME, signed, { httpOnly: true, sameSite: false, maxAge: MAX_AGE, path: '/' }); res.redirect('/'); } export function handleLogout(req, res) { const token = req.cookies?.[COOKIE_NAME]; if (token) { try { const payload = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString()); if (payload.jti) revokedTokens.add(payload.jti); } catch { /* ignore parse errors */ } } globalLogoutTimestamp = Date.now(); res.clearCookie(COOKIE_NAME); res.redirect('/login'); } // Require re-authentication for destructive actions export function requireReAuth(req, res, next) { const { reauth_token } = req.body || {}; const expected = Buffer.from(TOKEN_SECRET, 'utf8'); const provided = Buffer.from(reauth_token || '', 'utf8'); if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) { return res.status(403).send('Re-authentication required. Please re-enter your admin token.'); } next(); }