import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { JWT_CONFIG } from '../config/env.config'; import { getRedisClient } from '../util/redis.util'; declare global { namespace Express { interface Request { userId?: string; user?: any; } } } const auth = async (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ message: 'No token provided.' }); } const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, JWT_CONFIG.secret); if (typeof decoded === 'string' || !('userId' in decoded)) { return res.status(401).json({ message: 'Invalid token structure.' }); } // Check if session exists in Redis (for logout/invalidation) const redis = await getRedisClient(); const sessionKey = `session:${decoded.userId}:${token.substring(0, 10)}`; const sessionExists = await redis.exists(sessionKey); if (!sessionExists) { return res.status(401).json({ message: 'Session expired or invalid.' }); } // Fetch user to check 2FA status const User = (await import('../model/user.model')).default; const user = await User.findById(decoded.userId); if (!user) { return res.status(401).json({ message: 'User not found.' }); } req.userId = decoded.userId; req.user = user; // Enforce 2FA // Allow 2FA setup routes even if 2FA is not enabled const is2FASetupRoute = req.originalUrl.includes('/auth/2fa/'); if (!user.isTwoFactorEnabled && !is2FASetupRoute) { return res.status(403).json({ message: '2FA setup required.', code: '2fa_setup_required' }); } // Enforce document upload for trainers and organizations // Allow document upload routes and verification status check routes const isDocumentUploadRoute = req.originalUrl.includes('/users/upload-documents'); const isVerificationRoute = req.originalUrl.includes('/users/me') || req.originalUrl.includes('/users/reapply-verification'); const requiresDocumentUpload = (user.role === 'trainer' || user.role === 'organization') && !user.documentsUploaded; if (requiresDocumentUpload && !isDocumentUploadRoute && !isVerificationRoute && !is2FASetupRoute) { return res.status(403).json({ message: 'Document upload required.', code: 'document_upload_required' }); } // Extend session TTL on each request (sliding expiration) await redis.expire(sessionKey, 7 * 24 * 60 * 60); // 7 days next(); } catch (err) { return res.status(401).json({ message: 'Invalid token.' }); } }; export default auth;