const rateLimit = require("express-rate-limit"); const logger = require("../utils/logger"); // General rate limiter for Maps API endpoints const createMapsRateLimiter = (windowMs, max, message) => { return rateLimit({ windowMs, // time window in milliseconds max, // limit each IP/user to max requests per windowMs message: { error: message, retryAfter: Math.ceil(windowMs / 1000), // seconds }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers // Use user ID if available, otherwise fall back to IPv6-safe IP handling keyGenerator: (req) => { if (req.user?.id) { return `user:${req.user.id}`; } // Use the built-in IP key generator which properly handles IPv6 return rateLimit.defaultKeyGenerator(req); }, }); }; // Specific rate limiters for different endpoints const rateLimiters = { // Places Autocomplete - allow more requests since users type frequently placesAutocomplete: createMapsRateLimiter( 60 * 1000, // 1 minute window 30, // 30 requests per minute per user/IP "Too many autocomplete requests. Please slow down." ), // Place Details - moderate limit since each selection triggers this placeDetails: createMapsRateLimiter( 60 * 1000, // 1 minute window 20, // 20 requests per minute per user/IP "Too many place detail requests. Please slow down." ), // Geocoding - lower limit since this is typically used less frequently geocoding: createMapsRateLimiter( 60 * 1000, // 1 minute window 10, // 10 requests per minute per user/IP "Too many geocoding requests. Please slow down." ), }; // Enhanced rate limiter with user-specific limits const createUserBasedRateLimiter = (windowMs, max, message) => { const store = new Map(); // Simple in-memory store (use Redis in production) return (req, res, next) => { const key = req.user?.id ? `user:${req.user.id}` : rateLimit.defaultKeyGenerator(req); const now = Date.now(); const windowStart = now - windowMs; // Clean up old entries for (const [k, data] of store.entries()) { if (data.windowStart < windowStart) { store.delete(k); } } // Get or create user's request data let userData = store.get(key); if (!userData || userData.windowStart < windowStart) { userData = { count: 0, windowStart: now, resetTime: now + windowMs, }; } // Check if limit exceeded if (userData.count >= max) { return res.status(429).json({ error: message, retryAfter: Math.ceil((userData.resetTime - now) / 1000), }); } // Increment counter and store userData.count++; store.set(key, userData); // Add headers res.set({ "RateLimit-Limit": max, "RateLimit-Remaining": Math.max(0, max - userData.count), "RateLimit-Reset": new Date(userData.resetTime).toISOString(), }); next(); }; }; // Burst protection for expensive operations const burstProtection = createUserBasedRateLimiter( 10 * 1000, // 10 seconds 5, // 5 requests per 10 seconds "Too many requests in a short period. Please slow down." ); // Upload presign rate limiter - 30 requests per minute const uploadPresignLimiter = createUserBasedRateLimiter( 60 * 1000, // 1 minute window 30, // 30 presign requests per minute per user "Too many upload requests. Please slow down." ); // Helper to create a rate limit handler that logs the event const createRateLimitHandler = (limiterName) => (req, res, next, options) => { const reqLogger = logger.withRequestId(req.id); reqLogger.warn('Rate limit exceeded', { limiter: limiterName, ip: req.ip, userId: req.user?.id || 'anonymous', method: req.method, url: req.url, userAgent: req.get('User-Agent'), message: options.message?.error || 'Rate limit exceeded' }); res.status(options.statusCode).json(options.message); }; // Authentication rate limiters const authRateLimiters = { // Login rate limiter - stricter to prevent brute force login: rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 login attempts per 15 minutes message: { error: "Too many login attempts. Please try again in 15 minutes.", retryAfter: 900, // seconds }, standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: true, // Don't count successful logins handler: createRateLimitHandler('login'), }), // Registration rate limiter register: rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 3, // 3 registration attempts per hour message: { error: "Too many registration attempts. Please try again later.", retryAfter: 3600, }, standardHeaders: true, legacyHeaders: false, handler: createRateLimitHandler('register'), }), // Password reset rate limiter passwordReset: rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 3, // 3 password reset requests per hour message: { error: "Too many password reset requests. Please try again later.", retryAfter: 3600, }, standardHeaders: true, legacyHeaders: false, handler: createRateLimitHandler('passwordReset'), }), // Alpha code validation rate limiter alphaCodeValidation: rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 code validation attempts per 15 minutes message: { error: "Too many attempts. Please try again later.", retryAfter: 900, }, standardHeaders: true, legacyHeaders: false, handler: createRateLimitHandler('alphaCodeValidation'), }), // Email verification rate limiter - protect against brute force on 6-digit codes emailVerification: rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // 10 verification attempts per 15 minutes per IP message: { error: "Too many verification attempts. Please try again later.", retryAfter: 900, }, standardHeaders: true, legacyHeaders: false, handler: createRateLimitHandler('emailVerification'), }), // General API rate limiter general: rateLimit({ windowMs: 60 * 1000, // 1 minute max: 100, // 100 requests per minute message: { error: "Too many requests. Please slow down.", retryAfter: 60, }, standardHeaders: true, legacyHeaders: false, handler: createRateLimitHandler('general'), }), // Two-Factor Authentication rate limiters twoFactorVerification: rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // 10 verification attempts per 15 minutes message: { error: "Too many verification attempts. Please try again later.", retryAfter: 900, }, standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: true, handler: createRateLimitHandler('twoFactorVerification'), }), twoFactorSetup: rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 setup attempts per hour message: { error: "Too many setup attempts. Please try again later.", retryAfter: 3600, }, standardHeaders: true, legacyHeaders: false, handler: createRateLimitHandler('twoFactorSetup'), }), recoveryCode: rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 3, // 3 recovery code attempts per 15 minutes message: { error: "Too many recovery code attempts. Please try again later.", retryAfter: 900, }, standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, // Count all attempts for security handler: createRateLimitHandler('recoveryCode'), }), emailOtpSend: rateLimit({ windowMs: 10 * 60 * 1000, // 10 minutes max: 2, // 2 OTP sends per 10 minutes message: { error: "Please wait before requesting another code.", retryAfter: 600, }, standardHeaders: true, legacyHeaders: false, handler: createRateLimitHandler('emailOtpSend'), }), }; module.exports = { // Individual rate limiters placesAutocomplete: rateLimiters.placesAutocomplete, placeDetails: rateLimiters.placeDetails, geocoding: rateLimiters.geocoding, // Auth rate limiters loginLimiter: authRateLimiters.login, registerLimiter: authRateLimiters.register, passwordResetLimiter: authRateLimiters.passwordReset, alphaCodeValidationLimiter: authRateLimiters.alphaCodeValidation, emailVerificationLimiter: authRateLimiters.emailVerification, generalLimiter: authRateLimiters.general, // Two-Factor Authentication rate limiters twoFactorVerificationLimiter: authRateLimiters.twoFactorVerification, twoFactorSetupLimiter: authRateLimiters.twoFactorSetup, recoveryCodeLimiter: authRateLimiters.recoveryCode, emailOtpSendLimiter: authRateLimiters.emailOtpSend, // Burst protection burstProtection, // Upload rate limiter uploadPresignLimiter, // Utility functions createMapsRateLimiter, createUserBasedRateLimiter, };