const rateLimit = require("express-rate-limit"); const jwt = require('jsonwebtoken'); const prisma = require('../config/db'); const { mustEnrolTwoFactor } = require('../utils/twoFactor'); // The setup wizard's token may finish setup without 2FA for this long. const SETUP_TOKEN_GRACE_SECONDS = 2 * 60 * 60; // Protect routes - verify token const protect = async (req, res, next) => { let token; // Check if token exists in headers if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) { try { // Get token from header token = req.headers.authorization.split(' ')[1]; // Verify token — pin the algorithm so a token signed with an // unexpected/attacker-chosen algorithm is never accepted. const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); // Get user from the token (exclude password) req.user = await prisma.user.findUnique({ where: { id: decoded.id }, select: { id: true, name: true, email: true, role: true, isActive: true, createdAt: true, updatedAt: true, phoneNumber: true, tokenVersion: true, twoFactorEnabled: true } }); if (!req.user) { res.status(401); return next(new Error('User not found')); } if (!req.user.isActive) { res.status(401); return next(new Error('User account is deactivated')); } // An admin viewing as this user: only while the session is live, and read-only. if (decoded.imp) { const { liveSession } = require('../utils/impersonation'); const session = await liveSession(String(decoded.imp)); if (!session || session.targetUserId !== req.user.id) { return res.status(401).json({ message: 'The impersonation session has ended.', code: 'IMPERSONATION_ENDED' }); } req.impersonation = session; req.readOnly = true; } // Revocation check — tokenVersion in JWT must match DB // Old tokens without tokenVersion are treated as version 0 const tokenVer = decoded.tokenVersion ?? 0; if (tokenVer !== req.user.tokenVersion) { res.status(401); return next(new Error('Session has been revoked. Please log in again.')); } // Admins and supervisors must use 2FA. A session that predates the // requirement (or the user's promotion) is sent back to log in, where they // enrol. The setup wizard's own token gets a short grace period. const inSetupGrace = decoded.setup === true && Date.now() / 1000 - (decoded.iat || 0) < SETUP_TOKEN_GRACE_SECONDS; // (An impersonating admin has already passed 2FA themselves.) if (!inSetupGrace && !req.impersonation && (await mustEnrolTwoFactor(req.user))) { return res.status(401).json({ message: 'Two-factor authentication is required for your role. Please log in again to set it up.', code: 'TWO_FACTOR_REQUIRED', }); } next(); } catch (error) { console.error(error); res.status(401); return next(new Error('Not authorized, token failed')); } } else { res.status(401); return next(new Error('Not authorized, no token')); } }; // Admin only middleware const admin = (req, res, next) => { if (req.user && req.user.role === 'admin') { next(); } else { res.status(403); return next(new Error('Not authorized as an admin')); } }; // Staff or higher middleware const staff = (req, res, next) => { if (req.user && (req.user.role === 'admin' || req.user.role === 'supervisor' || req.user.role === 'staff')) { next(); } else { res.status(403); return next(new Error('Not authorized as staff')); } }; // Supervisor or higher middleware const supervisor = (req, res, next) => { if (req.user && (req.user.role === 'admin' || req.user.role === 'supervisor')) { next(); } else { res.status(403); return next(new Error('Not authorized as a supervisor')); } }; const loginLimiter = rateLimit({ windowMs: 60 * 1000, max: 15, message: "Too many login attempts. Try again later.", }); // Optional auth — populates req.user if a valid token is present, but never rejects the request const optionalAuth = async (req, res, next) => { if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer')) { return next(); } try { const token = req.headers.authorization.split(' ')[1]; const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); const user = await prisma.user.findUnique({ where: { id: decoded.id }, select: { id: true, name: true, email: true, role: true, isActive: true, createdAt: true, updatedAt: true, phoneNumber: true, tokenVersion: true } }); if (user && user.isActive && (decoded.tokenVersion ?? 0) === user.tokenVersion) { if (decoded.imp) { const { liveSession } = require('../utils/impersonation'); const session = await liveSession(String(decoded.imp)); req.readOnly = true; if (session && session.targetUserId === user.id) { req.user = user; req.impersonation = session; } } else { req.user = user; } } } catch { // Token invalid or expired — proceed without user } next(); }; module.exports = { protect, admin, staff, supervisor, loginLimiter, optionalAuth };