/** * Rate Limiter Middleware * * Provides centralized rate limiting for API endpoints using a configurable * memory store with optional Redis support for horizontal scaling. * * Usage: * import { authLimiter, apiLimiter, uploadLimiter } from './middlewares/rateLimiter.ts'; * app.use('/api/auth', authLimiter); * app.use('/api', apiLimiter); */ import type { RequestHandler } from 'express'; import type { RateLimitEntry, RateLimiterOptions } from '../types/index.ts'; import { logger } from '../utils/logger.ts'; import { getCurrentUser } from '../utils/request-context.ts'; // In-memory store for rate limiting // For horizontal scaling, replace with Redis store const rateLimitStore = new Map(); // Cleanup interval for expired entries (every 5 minutes) const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // Periodic cleanup of expired entries setInterval(() => { const now = Date.now(); let cleaned = 0; for (const [key, entry] of rateLimitStore.entries()) { if (entry.expiresAt <= now) { rateLimitStore.delete(key); cleaned++; } } if (cleaned > 0) { logger.debug({ cleaned }, 'Rate limit store cleanup'); } }, CLEANUP_INTERVAL_MS); /** * Create a rate limiter middleware * * @param {Object} options - Configuration options * @param {string} options.keyPrefix - Prefix for rate limit keys (e.g., 'auth', 'api') * @param {number} options.windowMs - Time window in milliseconds (default: 15 minutes) * @param {number} options.max - Maximum requests per window (default: 100) * @param {string} [options.message] - Custom error message * @param {boolean} [options.skipFailedRequests] - Don't count failed requests (status >= 400) * @param {Function} [options.keyGenerator] - Custom key generator (req) => string * @param {Function} [options.skip] - Skip rate limiting for certain requests (req) => boolean * @returns {Function} Express middleware */ const createRateLimiter = (options: RateLimiterOptions = {}): RequestHandler => { const { keyPrefix = 'rate-limit', windowMs = 15 * 60 * 1000, // 15 minutes max = 100, message = 'Too many requests. Please try again later.', skipFailedRequests = false, keyGenerator = null, skip = null, } = options; return (req, res, next) => { // Allow skipping rate limiting for certain requests if (skip && skip(req)) { return next(); } // Skip in development when accessing from localhost (optional) if ( process.env.NODE_ENV === 'development' && (req.ip === '127.0.0.1' || req.ip === '::1') ) { return next(); } // Generate rate limit key const clientKey = keyGenerator ? keyGenerator(req) : req.ip || req.connection?.remoteAddress || 'unknown'; const key = `${keyPrefix}:${clientKey}`; const now = Date.now(); // Get or create rate limit entry let entry = rateLimitStore.get(key); if (!entry || entry.expiresAt <= now) { // Create new entry entry = { count: 0, expiresAt: now + windowMs, resetTime: new Date(now + windowMs).toISOString(), }; } // Add standard rate limit headers const remaining = Math.max(0, max - entry.count - 1); const retryAfter = Math.ceil((entry.expiresAt - now) / 1000); res.setHeader('X-RateLimit-Limit', max); res.setHeader('X-RateLimit-Remaining', remaining); res.setHeader('X-RateLimit-Reset', entry.resetTime); // Check if rate limit exceeded if (entry.count >= max) { res.setHeader('Retry-After', retryAfter); logger.warn( { ip: clientKey, keyPrefix, count: entry.count, max, retryAfter, }, 'Rate limit exceeded', ); return res.status(429).json({ error: 'Too Many Requests', message, retryAfter, }); } // Increment count entry.count += 1; rateLimitStore.set(key, entry); // If skipFailedRequests is enabled, decrement on failed response if (skipFailedRequests) { const originalSend = res.send.bind(res); res.send = function sendWithRateLimitRefund(body?: unknown) { if (res.statusCode >= 400) { const currentEntry = rateLimitStore.get(key); if (currentEntry && currentEntry.count > 0) { currentEntry.count -= 1; rateLimitStore.set(key, currentEntry); } } return originalSend(body); }; } next(); }; }; /** * Create a rate limiter that uses both IP and user ID as key * Useful for authenticated endpoints */ const createAuthenticatedRateLimiter = ( options: RateLimiterOptions = {}, ): RequestHandler => { return createRateLimiter({ ...options, keyGenerator: (req) => { const userId = getCurrentUser(req)?.id || 'anonymous'; const ip = req.ip || 'unknown'; return `${ip}:${userId}`; }, }); }; // Pre-configured limiters for common use cases /** * Auth limiter - Strict limits for authentication endpoints * 5 requests per 15 minutes per IP */ const authLimiter = createRateLimiter({ keyPrefix: 'auth', windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // Allow 10 attempts per window message: 'Too many authentication attempts. Please try again later.', skipFailedRequests: false, // Count failed attempts }); /** * Password reset limiter - Prevent password reset abuse * 5 requests per hour per IP */ const passwordResetLimiter = createRateLimiter({ keyPrefix: 'password-reset', windowMs: 60 * 60 * 1000, // 1 hour max: 5, message: 'Too many password reset requests. Please try again later.', }); /** * General API limiter - Standard limits for API endpoints * 100 requests per minute per IP */ const apiLimiter = createRateLimiter({ keyPrefix: 'api', windowMs: 60 * 1000, // 1 minute max: 100, message: 'Too many requests. Please slow down.', skipFailedRequests: true, // Don't penalize for errors }); /** * Upload limiter - Limits for file uploads * 200 uploads per minute per IP (supports batch uploads of 100+ files) */ const uploadLimiter = createRateLimiter({ keyPrefix: 'upload', windowMs: 60 * 1000, // 1 minute max: 200, message: 'Too many file uploads. Please wait before uploading more.', skipFailedRequests: true, // Don't penalize for failed uploads }); /** * Download limiter - Permissive limits for file downloads * 1000 requests per minute per IP (supports offline mode downloading full presentations) */ const downloadLimiter = createRateLimiter({ keyPrefix: 'download', windowMs: 60 * 1000, // 1 minute max: 1000, message: 'Too many download requests. Please slow down.', skipFailedRequests: true, // Don't penalize for errors }); /** * Search limiter - Prevent search abuse * 30 searches per minute per IP */ const searchLimiter = createRateLimiter({ keyPrefix: 'search', windowMs: 60 * 1000, // 1 minute max: 30, message: 'Too many search requests. Please slow down.', }); export { createRateLimiter, createAuthenticatedRateLimiter, authLimiter, passwordResetLimiter, apiLimiter, uploadLimiter, downloadLimiter, searchLimiter, };