import { Request, Response, NextFunction } from 'express'; /** * Simple in-memory rate limiter * Tracks requests per API key */ class RateLimiter { private requests: Map = new Map(); private readonly limit: number; private readonly windowMs: number; constructor(limit: number = 100, windowMs: number = 60 * 60 * 1000) { this.limit = limit; this.windowMs = windowMs; // Cleanup old entries every 5 minutes setInterval(() => this.cleanup(), 5 * 60 * 1000); } check(keyId: string): { allowed: boolean; remaining: number; resetAt: number } { const now = Date.now(); const record = this.requests.get(keyId); if (!record || record.resetAt < now) { // Create new window const resetAt = now + this.windowMs; this.requests.set(keyId, { count: 1, resetAt }); return { allowed: true, remaining: this.limit - 1, resetAt }; } if (record.count >= this.limit) { return { allowed: false, remaining: 0, resetAt: record.resetAt }; } // Increment counter record.count++; return { allowed: true, remaining: this.limit - record.count, resetAt: record.resetAt, }; } private cleanup() { const now = Date.now(); for (const [keyId, record] of this.requests.entries()) { if (record.resetAt < now) { this.requests.delete(keyId); } } } } const rateLimiter = new RateLimiter(100, 60 * 60 * 1000); // 100 requests per hour /** * Rate limiting middleware * Must be used AFTER validateApiKey middleware */ export function rateLimitByApiKey(req: Request, res: Response, next: NextFunction): void { if (!req.apiKey) { next(); return; } const result = rateLimiter.check(req.apiKey.id); // Add rate limit headers res.setHeader('X-RateLimit-Limit', '100'); res.setHeader('X-RateLimit-Remaining', result.remaining.toString()); res.setHeader('X-RateLimit-Reset', new Date(result.resetAt).toISOString()); if (!result.allowed) { const retryAfter = Math.ceil((result.resetAt - Date.now()) / 1000); console.warn( `[${new Date().toISOString()}] Rate limit exceeded: ${req.apiKey.id} (${req.apiKey.keyType}) - reset: ${new Date(result.resetAt).toISOString()}`, ); res .status(429) .setHeader('Retry-After', retryAfter.toString()) .json({ error: 'Rate limit exceeded', message: `Too many requests. Retry after ${retryAfter} seconds`, retryAfter, }); return; } next(); }