import { Injectable, Inject } from '@nestjs/common'; import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider'; import { IRedisClient } from '../../core/configs/cache/IRedisClient'; export interface RateLimitConfig { maxAttempts: number; windowMs: number; blockDurationMs: number; } @Injectable() export class RateLimitingService { private readonly defaultConfig: RateLimitConfig = { maxAttempts: 5, // 5 tentativas windowMs: 15 * 60 * 1000, // 15 minutos blockDurationMs: 30 * 60 * 1000, // 30 minutos de bloqueio }; constructor( @Inject(RedisClientToken) private readonly redis: IRedisClient, ) {} /** * Verifica se o IP pode fazer uma tentativa de login * @param ip Endereço IP do cliente * @param config Configuração personalizada (opcional) * @returns true se permitido, false se bloqueado */ async isAllowed(ip: string, config?: Partial): Promise { const finalConfig = { ...this.defaultConfig, ...config }; const key = this.buildAttemptKey(ip); const blockKey = this.buildBlockKey(ip); // Verifica se está bloqueado const isBlocked = await this.redis.get(blockKey); if (isBlocked) { return false; } // Conta tentativas na janela de tempo const attempts = await this.redis.get(key); const attemptCount = attempts ? parseInt(attempts) : 0; if (attemptCount >= finalConfig.maxAttempts) { // Bloqueia o IP await this.redis.set(blockKey, 'blocked', finalConfig.blockDurationMs / 1000); return false; } return true; } /** * Registra uma tentativa de login * @param ip Endereço IP do cliente * @param success true se login foi bem-sucedido * @param config Configuração personalizada (opcional) */ async recordAttempt(ip: string, success: boolean, config?: Partial): Promise { const finalConfig = { ...this.defaultConfig, ...config }; const key = this.buildAttemptKey(ip); if (success) { await this.redis.del(key); } else { const attempts = await this.redis.get(key); const attemptCount = attempts ? parseInt(attempts) + 1 : 1; await this.redis.set(key, attemptCount.toString(), finalConfig.windowMs / 1000); } } /** * Obtém informações sobre tentativas de um IP * @param ip Endereço IP do cliente * @returns Informações sobre tentativas */ async getAttemptInfo(ip: string): Promise<{ attempts: number; isBlocked: boolean; remainingTime?: number; }> { const key = this.buildAttemptKey(ip); const blockKey = this.buildBlockKey(ip); const attempts = await this.redis.get(key); const isBlocked = await this.redis.get(blockKey); const ttl = await this.redis.ttl(blockKey); return { attempts: attempts ? parseInt(attempts) : 0, isBlocked: !!isBlocked, remainingTime: isBlocked ? ttl : undefined, }; } /** * Limpa tentativas de um IP (útil para testes ou admin) * @param ip Endereço IP do cliente */ async clearAttempts(ip: string): Promise { const key = this.buildAttemptKey(ip); const blockKey = this.buildBlockKey(ip); await this.redis.del(key); await this.redis.del(blockKey); } /** * Constrói a chave para armazenar tentativas * @param ip Endereço IP * @returns Chave para o Redis */ private buildAttemptKey(ip: string): string { return `auth:rate_limit:attempts:${ip}`; } /** * Constrói a chave para armazenar bloqueio * @param ip Endereço IP * @returns Chave para o Redis */ private buildBlockKey(ip: string): string { return `auth:rate_limit:blocked:${ip}`; } }