const rateLimit = require('express-rate-limit'); // In-memory store for per-recipient email tracking const emailTracker = new Map(); // Helper function to clean up expired entries function cleanupExpiredEntries() { const now = Date.now(); for (const [key, timestamp] of emailTracker.entries()) { if (now - timestamp > 5 * 60 * 1000) { // 5 minutes emailTracker.delete(key); } } } // Clean up expired entries every minute setInterval(cleanupExpiredEntries, 60 * 1000); // Custom key generator that's safer with trust proxy const safeKeyGenerator = (req) => { return req.ip || req.connection?.remoteAddress || 'unknown'; }; // Production-grade rate limiting configuration const rateLimitConfig = { // Email sending - very restrictive (5 emails per hour per IP) emailSend: { windowMs: 60 * 60 * 1000, // 1 hour max: 5, message: { error: 'Email rate limit exceeded', message: 'Too many emails sent from this IP. Maximum 5 emails per hour allowed.', retryAfter: 3600 } }, // Representative lookup - moderate (30 lookups per minute) representativeLookup: { windowMs: 60 * 1000, // 1 minute max: 30, message: { error: 'Representative lookup rate limit exceeded', message: 'Too many representative lookups. Maximum 30 per minute allowed.', retryAfter: 60 } }, // Login attempts - strict (5 attempts per 15 minutes) login: { windowMs: 15 * 60 * 1000, // 15 minutes max: 5, message: { error: 'Login rate limit exceeded', message: 'Too many login attempts. Please try again in 15 minutes.', retryAfter: 900 } }, // Public API - standard (100 requests per minute) publicAPI: { windowMs: 60 * 1000, // 1 minute max: 100, message: { error: 'API rate limit exceeded', message: 'Too many requests from this IP. Please try again later.', retryAfter: 60 } }, // Campaign creation/editing - moderate (10 per hour) campaignMutation: { windowMs: 60 * 60 * 1000, // 1 hour max: 10, message: { error: 'Campaign mutation rate limit exceeded', message: 'Too many campaign operations. Maximum 10 per hour allowed.', retryAfter: 3600 } }, // General fallback - legacy compatibility general: { windowMs: 15 * 60 * 1000, // 15 minutes max: 100, message: { error: 'Too many requests from this IP, please try again later.', retryAfter: 900 } } }; // Create rate limiter instances const emailSend = rateLimit({ ...rateLimitConfig.emailSend, standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, keyGenerator: safeKeyGenerator }); const representativeLookup = rateLimit({ ...rateLimitConfig.representativeLookup, standardHeaders: true, legacyHeaders: false, keyGenerator: safeKeyGenerator }); const login = rateLimit({ ...rateLimitConfig.login, standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, // Count all attempts keyGenerator: safeKeyGenerator }); const publicAPI = rateLimit({ ...rateLimitConfig.publicAPI, standardHeaders: true, legacyHeaders: false, keyGenerator: safeKeyGenerator }); const campaignMutation = rateLimit({ ...rateLimitConfig.campaignMutation, standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, keyGenerator: safeKeyGenerator }); // General API rate limiter (legacy - kept for backward compatibility) const general = rateLimit({ ...rateLimitConfig.general, standardHeaders: true, legacyHeaders: false, keyGenerator: safeKeyGenerator }); // Email sending rate limiter (legacy - kept for backward compatibility) const email = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 10, // limit each IP to 10 emails per hour message: { error: 'Too many emails sent from this IP, please try again later.', retryAfter: 60 * 60 // 1 hour in seconds }, standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, keyGenerator: safeKeyGenerator }); // Custom middleware for per-recipient email rate limiting const perRecipientEmailLimiter = (req, res, next) => { const clientIp = req.ip || req.connection?.remoteAddress || 'unknown'; const recipientEmail = req.body.recipientEmail; if (!recipientEmail) { return next(); // Let validation middleware handle missing recipient } const trackingKey = `${clientIp}:${recipientEmail}`; const now = Date.now(); const lastSent = emailTracker.get(trackingKey); if (lastSent && (now - lastSent) < 5 * 60 * 1000) { // 5 minutes const timeRemaining = Math.ceil((5 * 60 * 1000 - (now - lastSent)) / 1000); return res.status(429).json({ success: false, error: 'Rate limit exceeded', message: `You can only send one email per representative every 5 minutes. Please wait ${Math.ceil(timeRemaining / 60)} more minutes before sending another email to this representative.`, retryAfter: timeRemaining, rateLimitType: 'per-recipient' }); } // Store the current timestamp for this IP-recipient combination emailTracker.set(trackingKey, now); next(); }; // Represent API rate limiter (more restrictive) const representAPI = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 60, // match the Represent API limit of 60 requests per minute message: { error: 'Represent API rate limit exceeded, please try again later.', retryAfter: 60 // 1 minute in seconds }, standardHeaders: true, legacyHeaders: false, keyGenerator: safeKeyGenerator }); module.exports = { // Legacy exports (backward compatibility) general, email, representAPI, // New granular rate limiters emailSend, representativeLookup, login, publicAPI, campaignMutation, perRecipientEmailLimiter, // Export config for testing/monitoring rateLimitConfig };