File size: 2,789 Bytes
9a92a42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | 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; |