/** * RATE LIMITER SERVICE - REFERENCE IMPLEMENTATION * * @description Servicio de rate limiting para proteger endpoints. * Implementación in-memory simple con soporte para diferentes estrategias. * * @usage * ```typescript * // En middleware o guard * const limiter = getRateLimiter({ windowMs: 60000, max: 100 }); * if (limiter.isRateLimited(req.ip)) { * throw new TooManyRequestsException(); * } * ``` * * @origin gamilit/apps/backend/src/shared/services/rate-limiter.service.ts */ /** * Configuración del rate limiter */ export interface RateLimitConfig { /** Ventana de tiempo en milisegundos */ windowMs: number; /** Máximo de requests por ventana */ max: number; /** Mensaje de error personalizado */ message?: string; /** Función para generar key (default: IP) */ keyGenerator?: (req: any) => string; } /** * Estado interno de un cliente */ interface ClientState { count: number; resetTime: number; } /** * Rate Limiter Factory * * @param config - Configuración del limiter * @returns Instancia del rate limiter */ export function getRateLimiter(config: RateLimitConfig): RateLimiter { const clients = new Map(); // Limpieza periódica de entradas expiradas const cleanup = setInterval(() => { const now = Date.now(); for (const [key, state] of clients.entries()) { if (state.resetTime <= now) { clients.delete(key); } } }, config.windowMs); return { /** * Verificar si un cliente está rate limited */ check(key: string): RateLimitResult { const now = Date.now(); const state = clients.get(key); // Cliente nuevo o ventana expirada if (!state || state.resetTime <= now) { clients.set(key, { count: 1, resetTime: now + config.windowMs, }); return { limited: false, remaining: config.max - 1, resetTime: now + config.windowMs, }; } // Incrementar contador state.count++; // Verificar límite if (state.count > config.max) { return { limited: true, remaining: 0, resetTime: state.resetTime, retryAfter: Math.ceil((state.resetTime - now) / 1000), }; } return { limited: false, remaining: config.max - state.count, resetTime: state.resetTime, }; }, /** * Resetear contador de un cliente */ reset(key: string): void { clients.delete(key); }, /** * Limpiar todos los contadores */ clear(): void { clients.clear(); }, /** * Destruir el limiter (detener cleanup) */ destroy(): void { clearInterval(cleanup); clients.clear(); }, }; } /** * Interfaz del rate limiter */ export interface RateLimiter { check(key: string): RateLimitResult; reset(key: string): void; clear(): void; destroy(): void; } /** * Resultado de verificación */ export interface RateLimitResult { limited: boolean; remaining: number; resetTime: number; retryAfter?: number; } /** * Middleware para Express/NestJS */ export function rateLimitMiddleware(config: RateLimitConfig) { const limiter = getRateLimiter(config); const keyGenerator = config.keyGenerator || ((req) => req.ip || 'unknown'); return (req: any, res: any, next: () => void) => { const key = keyGenerator(req); const result = limiter.check(key); // Agregar headers informativos res.setHeader('X-RateLimit-Limit', config.max); res.setHeader('X-RateLimit-Remaining', result.remaining); res.setHeader('X-RateLimit-Reset', result.resetTime); if (result.limited) { res.setHeader('Retry-After', result.retryAfter); res.status(429).json({ statusCode: 429, message: config.message || 'Too many requests', retryAfter: result.retryAfter, }); return; } next(); }; } /** * Error de rate limit */ export class TooManyRequestsError extends Error { public readonly statusCode = 429; public readonly retryAfter: number; constructor(retryAfter: number, message?: string) { super(message || 'Too many requests'); this.retryAfter = retryAfter; } }