File size: 9,344 Bytes
c904036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5a548f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c904036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5a548f8
c904036
 
 
 
 
 
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
require('dotenv').config();

const jwt = require('jsonwebtoken');
const { createClient } = require('redis');
const { logger } = require('./errorHandler');

// Redis client for token blacklisting
const redisClient = createClient({
  url: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
});

redisClient.on('error', (err) => logger.error('Redis Client Error', err));

(async () => {
  try {
    if (!redisClient.isOpen) await redisClient.connect();
  } catch (err) {
    logger.error('Redis connection failed for auth:', err);
  }
})();

/**
 * Middleware to authenticate JWT token from Authorization header.
 * Attaches user info to req.user if valid.
 */
async function authenticateToken(req, res, next) {
  try {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) {
      return res.status(401).json({ error: 'No token provided.' });
    }

    // Check if token is blacklisted (with Redis error handling)
    let isBlacklisted = false;
    try {
      if (redisClient && redisClient.isOpen) {
        const blacklistEntry = await redisClient.get(`blacklist:${token}`);
        isBlacklisted = !!blacklistEntry;
      }
    } catch (redisError) {
      logger.error('Redis blacklist check failed:', redisError);
      // Fail open: continue if Redis is down (security vs availability trade-off)
      // Change to 'return res.status(503)...' to fail closed for higher security
    }
    
    if (isBlacklisted) {
      return res.status(401).json({ error: 'Token has been revoked.' });
    }

    // Verify token with enhanced options
    jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'], // Only allow HS256 algorithm
      issuer: 'campverse',
      audience: 'campverse-users',
      clockTolerance: 30, // Allow 30 seconds clock skew
    }, async (err, user) => {
      if (err) {
        if (err.name === 'TokenExpiredError') {
          return res.status(401).json({ error: 'Token expired.' });
        } else if (err.name === 'JsonWebTokenError') {
          return res.status(401).json({ error: 'Invalid token.' });
        } else {
          return res.status(401).json({ error: 'Token verification failed.' });
        }
      }

      // Check if user still exists and is active
      if (!user || !user.id) {
        return res.status(401).json({ error: 'Invalid token payload.' });
      }

      // Check if session is blacklisted (for immediate logout when session is revoked)
      if (user.sessionId) {
        try {
          if (redisClient && redisClient.isOpen) {
            const sessionBlacklisted = await redisClient.get(`campverse:session:blacklist:${user.sessionId}`);
            if (sessionBlacklisted) {
              return res.status(401).json({ error: 'Session has been terminated.' });
            }
          }
        } catch (redisError) {
          logger.error('Redis session blacklist check failed:', redisError);
        }
      }

      // Validate user still exists in database and is active
      try {
        const User = require('../Models/User');
        const dbUser = await User.findById(user.id).select('_id roles isVerified name email institutionId');
        
        if (!dbUser) {
          return res.status(401).json({ error: 'User no longer exists.' });
        }
        
        // Update user object with latest data from database
        user.roles = dbUser.roles;
        user.isVerified = dbUser.isVerified;
        user.name = dbUser.name;
        user.email = dbUser.email;
        user.institutionId = dbUser.institutionId;
      } catch (dbError) {
        logger.error('User validation error:', dbError);
        return res.status(500).json({ error: 'Authentication validation failed.' });
      }

      req.user = user;
      req.token = token; // Store token for potential blacklisting
      next();
    });
  } catch (error) {
    logger.error('Authentication error:', error);
    return res.status(500).json({ error: 'Authentication failed.' });
  }
}

/**
 * Middleware to require a specific user role or one of multiple roles.
 * Usage: requireRole('platformAdmin') or requireRole(['verifier', 'platformAdmin'])
 */
function requireRole(role) {
  return (req, res, next) => {
    if (!req.user || !req.user.roles) {
      return res.status(403).json({ error: 'Forbidden: insufficient role.' });
    }

    // Handle both single role and array of roles
    const requiredRoles = Array.isArray(role) ? role : [role];
    const hasRequiredRole = requiredRoles.some(r => req.user.roles.includes(r));

    if (!hasRequiredRole) {
      return res.status(403).json({ error: 'Forbidden: insufficient role.' });
    }
    next();
  };
}

/**
 * Middleware to allow if user is self or has one of the specified roles.
 * Usage: requireSelfOrRole(['platformAdmin', 'host'])
 */
function requireSelfOrRole(roles = []) {
  return (req, res, next) => {
    if (
      req.user &&
      (req.user.id === req.params.id ||
        roles.some((role) => req.user.roles.includes(role)))
    ) {
      return next();
    }
    return res.status(403).json({ error: 'Forbidden: not allowed.' });
  };
}

/**
 * Middleware to check if user has permission for specific action
 * Usage: requirePermission('event', 'create')
 */
function requirePermission(resource, action) {
  return (req, res, next) => {
    if (!req.user || !req.user.roles) {
      return res.status(403).json({ error: 'Forbidden: insufficient permissions.' });
    }

    // Define permission matrix
    const permissions = {
      platformAdmin: ['*'], // Platform admin can do everything
      verifier: ['event:verify', 'user:verify', 'institution:verify'],
      host: ['event:create', 'event:update', 'event:delete', 'event:manage'],
      student: ['event:view', 'event:join', 'profile:update']
    };

    const userPermissions = req.user.roles.flatMap(role => permissions[role] || []);
    
    if (userPermissions.includes('*') || userPermissions.includes(`${resource}:${action}`)) {
      return next();
    }

    return res.status(403).json({ error: `Forbidden: insufficient permissions for ${resource}:${action}` });
  };
}

/**
 * Logout function to blacklist token
 */
async function logout(req, res) {
  try {
    const token = req.token;
    if (token) {
      // Add token to blacklist with expiration
      const decoded = jwt.decode(token);
      const exp = decoded.exp || Math.floor(Date.now() / 1000) + 3600; // Default 1 hour
      const ttl = exp - Math.floor(Date.now() / 1000);
      
      if (ttl > 0) {
        await redisClient.setEx(`blacklist:${token}`, ttl, 'revoked');
      }
    }
    
    res.json({ message: 'Logged out successfully.' });
  } catch (error) {
    logger.error('Logout error:', error);
    res.status(500).json({ error: 'Logout failed.' });
  }
}

/**
 * Middleware that attempts to authenticate user but allows anonymous access.
 * Attaches user to req.user if valid token present.
 */
async function authenticateTokenOptional(req, res, next) {
  try {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) return next();

    // Check blacklist
    try {
      if (redisClient && redisClient.isOpen) {
        const isBlacklisted = await redisClient.get(`blacklist:${token}`);
        if (isBlacklisted) return next();
      }
    } catch (e) {
      logger.error('Redis error in optional auth:', e);
    }

    jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],
      issuer: 'campverse',
      audience: 'campverse-users',
      clockTolerance: 30,
    }, async (err, user) => {
      if (err || !user?.id) return next();

      // Check session
      try {
        if (user.sessionId && redisClient && redisClient.isOpen) {
          const sessionBlacklisted = await redisClient.get(`campverse:session:blacklist:${user.sessionId}`);
          if (sessionBlacklisted) return next();
        }
      } catch (e) {
        logger.error('Redis error in optional auth session check:', e);
      }

      // Check user in DB
      try {
        const User = require('../Models/User');
        const dbUser = await User.findById(user.id).select('_id roles isVerified name email institutionId');
        if (dbUser) {
          req.user = {
            id: dbUser._id,
            roles: dbUser.roles,
            isVerified: dbUser.isVerified,
            name: dbUser.name,
            email: dbUser.email,
            institutionId: dbUser.institutionId
          };
        }
      } catch (e) {
        // Ignore DB errors in optional auth
      }
      next();
    });
  } catch (err) {
    next();
  }
}

/**
 * Middleware to check if user is active and not suspended
 */
function requireActiveUser(req, res, next) {
  if (!req.user) {
    return res.status(401).json({ error: 'Authentication required.' });
  }

  // Check if user account is active
  if (req.user.accountStatus === 'suspended') {
    return res.status(403).json({ error: 'Account suspended.' });
  }

  if (req.user.accountStatus === 'deleted') {
    return res.status(403).json({ error: 'Account deleted.' });
  }

  next();
}

module.exports = {
  authenticateToken,
  authenticateTokenOptional,
  requireRole,
  requireSelfOrRole,
  requirePermission,
  requireActiveUser,
  logout,
};