199 lines
4.9 KiB
JavaScript
199 lines
4.9 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const bcrypt = require('bcryptjs');
|
|
const logger = require('../utils/logger');
|
|
const database = require('../config/database');
|
|
const redis = require('../config/redis');
|
|
|
|
// Protect routes - require authentication
|
|
const protect = async (req, res, next) => {
|
|
let token;
|
|
|
|
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
|
|
token = req.headers.authorization.split(' ')[1];
|
|
}
|
|
|
|
if (!token) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Not authorized to access this route'
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Verify token
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
|
|
// Check if user session exists in Redis
|
|
const session = await redis.getUserSession(decoded.id);
|
|
if (!session) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Session expired, please login again'
|
|
});
|
|
}
|
|
|
|
// Get user from database
|
|
const [users] = await database.query(
|
|
'SELECT id, username, email, role, status, created_at FROM users WHERE id = ? AND status = "active"',
|
|
[decoded.id]
|
|
);
|
|
|
|
if (users.length === 0) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'User not found or inactive'
|
|
});
|
|
}
|
|
|
|
req.user = users[0];
|
|
next();
|
|
} catch (error) {
|
|
logger.error('Token verification failed:', error);
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Not authorized to access this route'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Grant access to specific roles
|
|
const authorize = (...roles) => {
|
|
return (req, res, next) => {
|
|
if (!req.user) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'User not authenticated'
|
|
});
|
|
}
|
|
|
|
if (!roles.includes(req.user.role)) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
message: `User role ${req.user.role} is not authorized to access this route`
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
};
|
|
|
|
// Optional authentication - doesn't fail if no token
|
|
const optionalAuth = async (req, res, next) => {
|
|
let token;
|
|
|
|
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
|
|
token = req.headers.authorization.split(' ')[1];
|
|
}
|
|
|
|
if (!token) {
|
|
req.user = null;
|
|
return next();
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
const session = await redis.getUserSession(decoded.id);
|
|
|
|
if (session) {
|
|
const [users] = await database.query(
|
|
'SELECT id, username, email, role, status FROM users WHERE id = ? AND status = "active"',
|
|
[decoded.id]
|
|
);
|
|
|
|
if (users.length > 0) {
|
|
req.user = users[0];
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.debug('Optional auth failed:', error.message);
|
|
}
|
|
|
|
next();
|
|
};
|
|
|
|
// Rate limiting for authentication attempts
|
|
const authRateLimit = (req, res, next) => {
|
|
const key = `auth_attempts:${req.ip}`;
|
|
|
|
redis.get(key).then(attempts => {
|
|
const attemptCount = attempts ? parseInt(attempts) : 0;
|
|
|
|
if (attemptCount >= 5) {
|
|
return res.status(429).json({
|
|
success: false,
|
|
message: 'Too many authentication attempts. Please try again later.'
|
|
});
|
|
}
|
|
|
|
next();
|
|
}).catch(error => {
|
|
logger.error('Rate limit check failed:', error);
|
|
next(); // Continue if Redis is unavailable
|
|
});
|
|
};
|
|
|
|
// Log authentication attempts
|
|
const logAuthAttempt = (req, res, next) => {
|
|
const originalSend = res.send;
|
|
|
|
res.send = function(data) {
|
|
const response = JSON.parse(data);
|
|
|
|
if (response.success === false && req.path.includes('/login')) {
|
|
const key = `auth_attempts:${req.ip}`;
|
|
redis.get(key).then(attempts => {
|
|
const attemptCount = attempts ? parseInt(attempts) : 0;
|
|
redis.set(key, attemptCount + 1, 900); // 15 minutes
|
|
}).catch(error => {
|
|
logger.error('Failed to log auth attempt:', error);
|
|
});
|
|
|
|
logger.logSecurity('failed_login', {
|
|
ip: req.ip,
|
|
userAgent: req.get('User-Agent'),
|
|
username: req.body.username
|
|
}, 'warn');
|
|
} else if (response.success === true && req.path.includes('/login')) {
|
|
logger.logSecurity('successful_login', {
|
|
ip: req.ip,
|
|
userAgent: req.get('User-Agent'),
|
|
username: req.body.username
|
|
}, 'info');
|
|
}
|
|
|
|
originalSend.call(this, data);
|
|
};
|
|
|
|
next();
|
|
};
|
|
|
|
// Generate JWT token
|
|
const generateToken = (userId) => {
|
|
return jwt.sign(
|
|
{ id: userId },
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
|
);
|
|
};
|
|
|
|
// Hash password
|
|
const hashPassword = async (password) => {
|
|
const salt = await bcrypt.genSalt(10);
|
|
return bcrypt.hash(password, salt);
|
|
};
|
|
|
|
// Compare password
|
|
const comparePassword = async (password, hashedPassword) => {
|
|
return bcrypt.compare(password, hashedPassword);
|
|
};
|
|
|
|
module.exports = {
|
|
protect,
|
|
authorize,
|
|
optionalAuth,
|
|
authRateLimit,
|
|
logAuthAttempt,
|
|
generateToken,
|
|
hashPassword,
|
|
comparePassword
|
|
};
|