from fastapi import HTTPException, Request, status from redis.exceptions import RedisError from app.utils.redis_client import get_redis_client def _safe_ip(request: Request) -> str: if not request.client or not request.client.host: return "unknown" return request.client.host async def enforce_ip_rate_limit( request: Request, *, scope: str, limit: int, window_seconds: int = 60, ) -> None: if limit <= 0: return key = f"rl:{scope}:ip:{_safe_ip(request)}" await _enforce(key=key, limit=limit, window_seconds=window_seconds) async def enforce_user_rate_limit( user_id: int, *, scope: str, limit: int, window_seconds: int = 60, ) -> None: if limit <= 0: return key = f"rl:{scope}:user:{user_id}" await _enforce(key=key, limit=limit, window_seconds=window_seconds) async def _enforce(*, key: str, limit: int, window_seconds: int) -> None: try: redis = get_redis_client() current = await redis.incr(key) if current == 1: await redis.expire(key, window_seconds) if current > limit: ttl = await redis.ttl(key) retry_after = max(1, ttl if ttl and ttl > 0 else window_seconds) raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=f"Rate limit exceeded. Retry in {retry_after} seconds.", ) except RedisError: # Fail-open in case of Redis outage to keep core API available. return