Shatteredvoid/src/middleware/rateLimit.middleware.js
MegaProxy e681c446b6 feat: implement comprehensive startup system and fix authentication
Major improvements:
- Created startup orchestration system with health monitoring and graceful shutdown
- Fixed user registration and login with simplified authentication flow
- Rebuilt authentication forms from scratch with direct API integration
- Implemented comprehensive debugging and error handling
- Added Redis fallback functionality for disabled environments
- Fixed CORS configuration for cross-origin frontend requests
- Simplified password validation to 6+ characters (removed complexity requirements)
- Added toast notifications at app level for better UX feedback
- Created comprehensive startup/shutdown scripts with OODA methodology
- Fixed database validation and connection issues
- Implemented TokenService memory fallback when Redis is disabled

Technical details:
- New SimpleLoginForm.tsx and SimpleRegisterForm.tsx components
- Enhanced CORS middleware with additional allowed origins
- Simplified auth validators and removed strict password requirements
- Added extensive logging and diagnostic capabilities
- Fixed authentication middleware token validation
- Implemented graceful Redis error handling throughout the stack
- Created modular startup system with configurable health checks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-03 12:53:25 +00:00

326 lines
8.6 KiB
JavaScript

/**
* Rate Limiting Middleware
* Implements comprehensive rate limiting with Redis backend and flexible configuration
*/
const rateLimit = require('express-rate-limit');
const { getRedisClient } = require('../config/redis');
const logger = require('../utils/logger');
// Rate limiting configuration
const RATE_LIMIT_CONFIG = {
// Global API rate limits
global: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000, // 1000 requests per window
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
skipFailedRequests: false,
},
// Authentication endpoints (more restrictive)
auth: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per window
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true, // Don't count successful logins
skipFailedRequests: false,
},
// Player API endpoints
player: {
windowMs: 1 * 60 * 1000, // 1 minute
max: 120, // 120 requests per minute
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
skipFailedRequests: false,
},
// Admin API endpoints (more lenient for legitimate admin users)
admin: {
windowMs: 1 * 60 * 1000, // 1 minute
max: 300, // 300 requests per minute
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
skipFailedRequests: false,
},
// Game action endpoints (prevent spam)
gameAction: {
windowMs: 30 * 1000, // 30 seconds
max: 30, // 30 actions per 30 seconds
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
skipFailedRequests: true,
},
// Message sending (prevent spam)
messaging: {
windowMs: 5 * 60 * 1000, // 5 minutes
max: 10, // 10 messages per 5 minutes
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
skipFailedRequests: true,
},
};
/**
* Create Redis store for rate limiting if Redis is available
* @returns {Object|null} Redis store or null if Redis unavailable
*/
function createRedisStore() {
// Check if Redis is disabled first
if (process.env.DISABLE_REDIS === 'true') {
logger.info('Redis disabled for rate limiting, using memory store');
return null;
}
try {
const redis = getRedisClient();
if (!redis) {
logger.warn('Redis not available for rate limiting, using memory store');
return null;
}
// Create Redis store for express-rate-limit
try {
const { RedisStore } = require('rate-limit-redis');
return new RedisStore({
sendCommand: (...args) => redis.sendCommand(args),
prefix: 'rl:', // Rate limit prefix
});
} catch (error) {
logger.warn('Failed to create RedisStore, falling back to memory store', {
error: error.message,
});
return null;
}
} catch (error) {
logger.warn('Failed to create Redis store for rate limiting', {
error: error.message,
});
return null;
}
}
/**
* Create key generator for rate limiting
* @param {string} prefix - Key prefix
* @returns {Function} Key generator function
*/
function createKeyGenerator(prefix = 'global') {
return (req) => {
const ip = req.ip || req.connection.remoteAddress || 'unknown';
const userId = req.user?.playerId || req.user?.adminId || 'anonymous';
return `${prefix}:${userId}:${ip}`;
};
}
/**
* Create rate limit handler
* @param {string} type - Rate limit type for logging
* @returns {Function} Rate limit handler function
*/
function createRateLimitHandler(type) {
return (req, res) => {
const correlationId = req.correlationId;
const ip = req.ip || req.connection.remoteAddress;
const userId = req.user?.playerId || req.user?.adminId;
const userType = req.user?.type || 'anonymous';
logger.warn('Rate limit exceeded', {
correlationId,
type,
ip,
userId,
userType,
path: req.path,
method: req.method,
userAgent: req.get('User-Agent'),
retryAfter: res.get('Retry-After'),
});
return res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
type,
retryAfter: res.get('Retry-After'),
correlationId,
});
};
}
/**
* Create skip function for rate limiting
* @param {Array<string>} skipPaths - Paths to skip rate limiting
* @param {Array<string>} skipIPs - IPs to skip rate limiting
* @returns {Function} Skip function
*/
function createSkipFunction(skipPaths = [], skipIPs = []) {
return (req) => {
const ip = req.ip || req.connection.remoteAddress;
// Skip health checks
if (req.path === '/health' || req.path === '/api/health') {
return true;
}
// Skip specified paths
if (skipPaths.some(path => req.path.startsWith(path))) {
return true;
}
// Skip specified IPs (for development/testing)
if (skipIPs.includes(ip)) {
return true;
}
// Skip if rate limiting is disabled
if (process.env.DISABLE_RATE_LIMITING === 'true') {
return true;
}
return false;
};
}
/**
* Create rate limiter middleware
* @param {string} type - Type of rate limiter
* @param {Object} customConfig - Custom configuration
* @returns {Function} Rate limiter middleware
*/
function createRateLimiter(type, customConfig = {}) {
const config = { ...RATE_LIMIT_CONFIG[type], ...customConfig };
const store = createRedisStore();
const rateLimiter = rateLimit({
...config,
store,
keyGenerator: createKeyGenerator(type),
handler: createRateLimitHandler(type),
skip: createSkipFunction(),
// Note: onLimitReached is deprecated in express-rate-limit v7
// Removed for compatibility
});
// Log rate limiter creation
logger.info('Rate limiter created', {
type,
windowMs: config.windowMs,
max: config.max,
useRedis: !!store,
});
return rateLimiter;
}
/**
* Pre-configured rate limiters
*/
const rateLimiters = {
global: createRateLimiter('global'),
auth: createRateLimiter('auth'),
player: createRateLimiter('player'),
admin: createRateLimiter('admin'),
gameAction: createRateLimiter('gameAction'),
messaging: createRateLimiter('messaging'),
};
/**
* Middleware to add rate limit headers even when not limiting
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function addRateLimitHeaders(req, res, next) {
// Add custom headers for client information
res.set({
'X-RateLimit-Policy': 'See API documentation for rate limiting details',
});
next();
}
/**
* Custom rate limiter for WebSocket connections
* @param {number} maxConnections - Maximum connections per IP
* @param {number} windowMs - Time window in milliseconds
* @returns {Function} WebSocket rate limiter function
*/
function createWebSocketRateLimiter(maxConnections = 10, windowMs = 60000) {
const connections = new Map();
return (socket, next) => {
const ip = socket.handshake.address;
const now = Date.now();
// Clean up old connections
if (connections.has(ip)) {
const connectionTimes = connections.get(ip).filter(time => now - time < windowMs);
connections.set(ip, connectionTimes);
}
// Check rate limit
const currentConnections = connections.get(ip) || [];
if (currentConnections.length >= maxConnections) {
logger.warn('WebSocket connection rate limit exceeded', {
ip,
currentConnections: currentConnections.length,
maxConnections,
});
return next(new Error('Connection rate limit exceeded'));
}
// Add current connection
currentConnections.push(now);
connections.set(ip, currentConnections);
logger.debug('WebSocket connection allowed', {
ip,
connections: currentConnections.length,
maxConnections,
});
next();
};
}
/**
* Middleware to apply different rate limits based on user type
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function dynamicRateLimit(req, res, next) {
const userType = req.user?.type;
let limiter;
if (userType === 'admin') {
limiter = rateLimiters.admin;
} else if (userType === 'player') {
limiter = rateLimiters.player;
} else {
limiter = rateLimiters.global;
}
return limiter(req, res, next);
}
module.exports = {
rateLimiters,
createRateLimiter,
createWebSocketRateLimiter,
addRateLimitHeaders,
dynamicRateLimit,
RATE_LIMIT_CONFIG,
};