const rateLimit = require('express-rate-limit'); const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); const logger = require('../utils/logger'); // Cache for rate limit settings let settingsCache = null; let cacheExpiry = 0; const CACHE_DURATION = 60000; // 1 minute cache /** * Get rate limit settings from database with caching */ async function getRateLimitSettings() { try { // Check cache if (settingsCache && Date.now() < cacheExpiry) { return settingsCache; } // Fetch from database const settings = await db('app_settings') .whereIn('setting_key', [ 'rate_limit_enabled', 'rate_limit_window_minutes', 'rate_limit_max_requests', 'rate_limit_auth_max_requests', 'rate_limit_skip_authenticated', 'rate_limit_public_endpoints_only' ]); // Parse settings into object const config = { enabled: true, windowMinutes: 15, maxRequests: 100, authMaxRequests: 5, skipAuthenticated: true, publicEndpointsOnly: false }; settings.forEach(setting => { const value = JSON.parse(setting.setting_value); switch (setting.setting_key) { case 'rate_limit_enabled': config.enabled = value; break; case 'rate_limit_window_minutes': config.windowMinutes = value; break; case 'rate_limit_max_requests': config.maxRequests = value; break; case 'rate_limit_auth_max_requests': config.authMaxRequests = value; break; case 'rate_limit_skip_authenticated': config.skipAuthenticated = value; break; case 'rate_limit_public_endpoints_only': config.publicEndpointsOnly = value; break; } }); // Update cache settingsCache = config; cacheExpiry = Date.now() + CACHE_DURATION; return config; } catch (error) { logger.error('Failed to fetch rate limit settings:', error); // Return defaults on error return { enabled: true, windowMinutes: 15, maxRequests: 100, authMaxRequests: 5, skipAuthenticated: true, publicEndpointsOnly: false }; } } /** * Clear settings cache (call when settings are updated) */ function clearSettingsCache() { settingsCache = null; cacheExpiry = 0; } /** * Check if request has valid authentication */ function isAuthenticated(req) { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return false; } const token = authHeader.substring(7); const decoded = jwt.verify(token, process.env.JWT_SECRET); // Check if token is valid if (!decoded || typeof decoded !== 'object') { return false; } // Valid token found - check type req.tokenType = decoded.type; // 'admin' or 'gallery' req.tokenPayload = decoded; return true; } catch (error) { return false; } } /** * Determine if rate limiting should be applied to this request */ function shouldSkipRateLimit(req, config) { // If rate limiting is disabled globally if (!config.enabled) { return true; } // Never skip rate limiting for auth endpoints const isAuthEndpoint = req.path.match(/\/(auth|login|gallery\/[^/]+\/verify)$/); if (isAuthEndpoint) { return false; } // Check if we should skip authenticated requests if (config.skipAuthenticated && isAuthenticated(req)) { return true; } // Check if we only rate limit public endpoints if (config.publicEndpointsOnly) { const isPublicEndpoint = req.path.startsWith('/api/public/') || req.path.startsWith('/api/gallery/') || isAuthEndpoint; return !isPublicEndpoint; } return false; } /** * Create dynamic rate limiter */ async function createRateLimiter() { const config = await getRateLimitSettings(); return rateLimit({ windowMs: config.windowMinutes * 60 * 1000, max: async (req) => { // Refresh config for each request const currentConfig = await getRateLimitSettings(); // Different limits for auth endpoints const isAuthEndpoint = req.path.match(/\/(auth|login|gallery\/[^/]+\/verify)$/); return isAuthEndpoint ? currentConfig.authMaxRequests : currentConfig.maxRequests; }, keyGenerator: (req) => { // Use correct client IP when behind proxy return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.connection.remoteAddress || req.ip; }, skip: async (req) => { const currentConfig = await getRateLimitSettings(); return shouldSkipRateLimit(req, currentConfig); }, handler: (req, res) => { const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.connection.remoteAddress || req.ip; // Enhanced logging for production analysis logger.warn('Rate limit exceeded', { ip: clientIp, path: req.path, method: req.method, authenticated: isAuthenticated(req), tokenType: req.tokenType, userAgent: req.headers['user-agent'], referer: req.headers['referer'], origin: req.headers['origin'], timestamp: new Date().toISOString(), headers: { 'x-forwarded-for': req.headers['x-forwarded-for'], 'x-real-ip': req.headers['x-real-ip'] }, requestUrl: req.originalUrl, rateLimitInfo: { limit: req.rateLimit?.limit, current: req.rateLimit?.current, remaining: req.rateLimit?.remaining, resetTime: req.rateLimit?.resetTime ? new Date(req.rateLimit.resetTime).toISOString() : null } }); res.status(429).json({ error: 'Too many requests, please try again later.', retryAfter: res.getHeader('Retry-After') }); }, standardHeaders: true, // Return rate limit info in headers legacyHeaders: false, // Disable X-RateLimit headers }); } /** * Create auth-specific rate limiter */ async function createAuthRateLimiter() { const config = await getRateLimitSettings(); return rateLimit({ windowMs: config.windowMinutes * 60 * 1000, max: config.authMaxRequests, keyGenerator: (req) => { // Use correct client IP when behind proxy return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.connection.remoteAddress || req.ip; }, skip: async () => { const currentConfig = await getRateLimitSettings(); return !currentConfig.enabled; }, handler: (req, res) => { const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.connection.remoteAddress || req.ip; // Enhanced logging for auth failures logger.warn('Auth rate limit exceeded', { ip: clientIp, path: req.path, method: req.method, userAgent: req.headers['user-agent'], timestamp: new Date().toISOString(), headers: { 'x-forwarded-for': req.headers['x-forwarded-for'], 'x-real-ip': req.headers['x-real-ip'] }, requestUrl: req.originalUrl, authType: req.path.includes('admin') ? 'admin' : 'gallery', rateLimitInfo: { limit: req.rateLimit?.limit, current: req.rateLimit?.current, remaining: req.rateLimit?.remaining, resetTime: req.rateLimit?.resetTime ? new Date(req.rateLimit.resetTime).toISOString() : null } }); res.status(429).json({ error: 'Too many authentication attempts, please try again later.', retryAfter: res.getHeader('Retry-After') }); }, standardHeaders: true, legacyHeaders: false, }); } module.exports = { getRateLimitSettings, clearSettingsCache, createRateLimiter, createAuthRateLimiter, isAuthenticated, shouldSkipRateLimit };