const rateLimit = require("express-rate-limit"); const RedisStore = require("rate-limit-redis").default; const { getRedisClient, isRedisConnected } = require("../config/redis"); // Track if we've already logged the fallback warning let fallbackWarningLogged = false; // Simple in-memory store as fallback when Redis is not available class MemoryStore { constructor() { this.hits = new Map(); this.resetTime = new Map(); // Clean up old entries periodically to prevent memory leaks this.cleanupInterval = setInterval( () => { const now = Date.now(); for (const [key, resetTime] of this.resetTime.entries()) { if (now > resetTime) { this.hits.delete(key); this.resetTime.delete(key); } } }, 5 * 60 * 1000 ); // Clean up every 5 minutes } async increment(key, windowMs) { const now = Date.now(); const resetTime = this.resetTime.get(key); if (!resetTime || now > resetTime) { this.hits.set(key, 1); this.resetTime.set(key, now + windowMs); return { totalHits: 1, timeToExpire: windowMs }; } const hits = (this.hits.get(key) || 0) + 1; this.hits.set(key, hits); return { totalHits: hits, timeToExpire: resetTime - now }; } async decrement(key) { const hits = this.hits.get(key) || 0; if (hits > 0) { this.hits.set(key, hits - 1); } } async resetKey(key) { this.hits.delete(key); this.resetTime.delete(key); } } // Create store based on Redis availability const createStore = () => { try { if (isRedisConnected()) { const redisClient = getRedisClient(); return new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args), }); } else { throw new Error("Redis not connected"); } } catch (error) { // Only log the warning once to avoid spam if (!fallbackWarningLogged) { console.warn("Rate limiting: Using in-memory store (Redis unavailable)"); fallbackWarningLogged = true; } return new MemoryStore(); } }; // Create rate limiter for form submissions const createSubmissionRateLimiter = () => { return rateLimit({ store: createStore(), windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // Limit each IP to 10 requests per windowMs for any form message: { error: "Too many form submissions from this IP address. Please try again later.", }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers keyGenerator: (req) => { // Generate unique key per IP return `submit_ip:${req.ip}`; }, skip: (req) => { // Skip rate limiting for specific conditions if needed return false; }, }); }; // Create more restrictive rate limiter for specific form+IP combinations const createFormSpecificRateLimiter = () => { return rateLimit({ store: createStore(), windowMs: 5 * 60 * 1000, // 5 minutes max: 3, // Limit each IP to 3 requests per 5 minutes per specific form message: { error: "Too many submissions for this form from your IP address. Please try again later.", }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => { // Generate unique key per form+IP combination const formUuid = req.params.formUuid; return `submit_form:${formUuid}:${req.ip}`; }, skip: (req) => { // Skip rate limiting for specific conditions if needed return false; }, }); }; // Create a more aggressive rate limiter for potential abuse const createStrictRateLimiter = () => { return rateLimit({ store: createStore(), windowMs: 60 * 60 * 1000, // 1 hour max: 50, // Limit each IP to 50 requests per hour across all forms message: { error: "Too many requests from this IP address. Please try again later.", }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => { return `strict_ip:${req.ip}`; }, }); }; module.exports = { createSubmissionRateLimiter, createFormSpecificRateLimiter, createStrictRateLimiter, };