from fastapi import Request, HTTPException, status from typing import Optional, Tuple import logging from config.config import settings from services.redis_service import redis_service from security.fingerprint import get_client_ip logger = logging.getLogger(__name__) class RateLimiter: @staticmethod def _make_key(identifier: str, action: str) -> str: return f"{action}:{identifier}" @classmethod async def check_login_rate_limit( cls, email: str, ip_address: str ) -> Tuple[bool, Optional[str], int]: window_seconds = settings.rate_limit_login_window_minutes * 60 max_attempts = settings.rate_limit_login_attempts email_key = cls._make_key(email.lower(), "login_email") email_count = await redis_service.get_rate_limit_count(email_key) if email_count >= max_attempts: return ( False, f"Trop de tentatives pour cet email. Reessayez dans {settings.rate_limit_login_window_minutes} minutes.", window_seconds, ) ip_key = cls._make_key(ip_address, "login_ip") ip_count = await redis_service.get_rate_limit_count(ip_key) ip_limit = max_attempts * 3 if ip_count >= ip_limit: return ( False, window_seconds, ) return (True, None, 0) @classmethod async def record_login_attempt( cls, email: str, ip_address: str, success: bool ) -> None: window_seconds = settings.rate_limit_login_window_minutes * 60 if success: email_key = cls._make_key(email.lower(), "login_email") await redis_service.reset_rate_limit(email_key) logger.debug(f"Rate limit reinitialise pour {email}") else: email_key = cls._make_key(email.lower(), "login_email") await redis_service.increment_rate_limit(email_key, window_seconds) ip_key = cls._make_key(ip_address, "login_ip") await redis_service.increment_rate_limit(ip_key, window_seconds) logger.debug( f"Tentative echouee enregistree pour {email} depuis {ip_address}" ) @classmethod async def check_api_rate_limit( cls, identifier: str, endpoint: Optional[str] = None ) -> Tuple[bool, int, int]: window_seconds = settings.rate_limit_api_window_seconds max_requests = settings.rate_limit_api_requests if endpoint: key = cls._make_key(f"{identifier}:{endpoint}", "api") else: key = cls._make_key(identifier, "api") count = await redis_service.increment_rate_limit(key, window_seconds) remaining = max(0, max_requests - count) if count > max_requests: return (False, remaining, window_seconds) return (True, remaining, window_seconds) @classmethod async def check_password_reset_rate_limit( cls, email: str, ip_address: str ) -> Tuple[bool, Optional[str]]: window_seconds = 3600 max_attempts_email = 3 max_attempts_ip = 10 email_key = cls._make_key(email.lower(), "reset_email") email_count = await redis_service.get_rate_limit_count(email_key) if email_count >= max_attempts_email: return (False, "Trop de demandes de reinitialisation pour cet email.") ip_key = cls._make_key(ip_address, "reset_ip") ip_count = await redis_service.get_rate_limit_count(ip_key) if ip_count >= max_attempts_ip: return (False, "Trop de demandes depuis cette adresse IP.") await redis_service.increment_rate_limit(email_key, window_seconds) await redis_service.increment_rate_limit(ip_key, window_seconds) return (True, None) @classmethod async def check_registration_rate_limit( cls, ip_address: str ) -> Tuple[bool, Optional[str]]: window_seconds = 3600 max_registrations = 5 key = cls._make_key(ip_address, "register_ip") count = await redis_service.get_rate_limit_count(key) if count >= max_registrations: return (False, "Trop d'inscriptions depuis cette adresse IP.") await redis_service.increment_rate_limit(key, window_seconds) return (True, None) async def check_rate_limit_dependency(request: Request) -> None: ip = get_client_ip(request) allowed, remaining, reset_seconds = await RateLimiter.check_api_rate_limit(ip) request.state.rate_limit_remaining = remaining request.state.rate_limit_reset = reset_seconds if not allowed: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Limite de requetes atteinte. Reessayez plus tard.", headers={ "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": str(reset_seconds), "Retry-After": str(reset_seconds), }, )