/** * Rate Limiting Middleware * * Simple in-memory rate limiter. * For production, consider using Redis-backed solution. */ import { config } from '../config.js'; // In-memory store for request counts const requestCounts = new Map(); // Cleanup old entries periodically setInterval(() => { const now = Date.now(); for (const [key, data] of requestCounts) { if (now - data.windowStart > config.rateLimitWindowMs * 2) { requestCounts.delete(key); } } }, 60000); // Every minute /** * Get client identifier from request */ function getClientId(req) { // Use X-Forwarded-For header if behind proxy const forwarded = req.headers['x-forwarded-for']; if (forwarded) { return forwarded.split(',')[0].trim(); } return req.ip || (req.connection ? req.connection.remoteAddress : null) || 'unknown'; } /** * Rate limiting middleware */ export function rateLimit(req, res, next) { const clientId = getClientId(req); const now = Date.now(); let data = requestCounts.get(clientId); if (!data || now - data.windowStart > config.rateLimitWindowMs) { // New window data = { windowStart: now, count: 1 }; requestCounts.set(clientId, data); next(); return; } data.count++; if (data.count > config.rateLimitMaxRequests) { const retryAfter = Math.ceil((data.windowStart + config.rateLimitWindowMs - now) / 1000); res.set('Retry-After', retryAfter.toString()); res.set('X-RateLimit-Limit', config.rateLimitMaxRequests.toString()); res.set('X-RateLimit-Remaining', '0'); res.set('X-RateLimit-Reset', new Date(data.windowStart + config.rateLimitWindowMs).toISOString()); return res.status(429).json({ error: 'Too many requests', retry_after: retryAfter }); } // Add rate limit headers res.set('X-RateLimit-Limit', config.rateLimitMaxRequests.toString()); res.set('X-RateLimit-Remaining', (config.rateLimitMaxRequests - data.count).toString()); res.set('X-RateLimit-Reset', new Date(data.windowStart + config.rateLimitWindowMs).toISOString()); next(); }