/*********************************************************************************************************************************************************************** * File Name: auth.middleware.js * Type of Program: Middleware * Description: JWT authentication guard. * Reads the Bearer token from the Authorization header, * verifies it, attaches decoded payload to req.user, * and verifies the session is still active in the DB. * Author: rgrgogu * Date Created: Oct. 6, 2025 *********************************************************************************************************************************************************************** * HOW TO USE: * const { authenticate } = require('../middleware/auth.middleware'); * router.get('/profile', authenticate, handler); ***********************************************************************************************************************************************************************/ const { verifyAccessToken } = require('../utils/token.util'); const mdl_Users = require('../models/users/users.mdl'); const R = require('../utils/response.util'); const { checkAccountStatus } = require('../services/accountStatus.service'); /** * Validates JWT and loads user from DB. * Attaches the full user record to req.user. */ const authenticate = async (req, res, next) => { try { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) return R.error(res, 'No token provided.', 401); const token = authHeader.split(' ')[1]; const decoded = verifyAccessToken(token); const user = await mdl_Users.findByPk(decoded.user_id, { attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] }, }); if (!user) return R.error(res, 'User not found.', 401); const status = await checkAccountStatus(user); if (!status.ok) { if (status.code === 'deactivated') return R.error(res, 'Account is deactivated.', 403); return R.error(res, 'Your account has been suspended.', 403, { banned: true, ban_expires_at: status.ban_expires_at, }); } req.user = user; next(); } catch (err) { if (err.name === 'TokenExpiredError') return R.error(res, 'Token expired. Please log in again.', 401); return R.error(res, 'Invalid token.', 401); } }; /** * Soft authentication for endpoints that are useful both authenticated and unauthenticated. * * Behaviour: * - No Authorization header → guest mode (req.user = null, continues) * - Valid token → authenticated (req.user = user, continues) * - Present but invalid/expired token → 401 (a token was attempted; reject it) * * The distinction between "no token" and "bad token" matters: * silently ignoring a malformed token would allow it to be used as a scapegoat * to probe the endpoint without being flagged as unauthenticated. */ const softAuthenticate = async (req, res, next) => { const authHeader = req.headers.authorization; // No token at all → guest mode if (!authHeader?.startsWith('Bearer ')) { req.user = null; return next(); } // Token present → validate it fully (same rules as authenticate) try { const token = authHeader.split(' ')[1]; const decoded = verifyAccessToken(token); const user = await mdl_Users.findByPk(decoded.user_id, { attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] }, }); if (!user) { req.user = null; return next(); } const status = await checkAccountStatus(user); if (!status.ok) { req.user = null; return next(); } req.user = user; next(); } catch (err) { if (err.name === 'TokenExpiredError') return R.error(res, 'Token expired. Please log in again.', 401); return R.error(res, 'Invalid token.', 401); } }; module.exports = { authenticate, softAuthenticate };