# core/rate_limit.py from fastapi import Request, HTTPException, status from db.redis import redis_client class RateLimiter: def __init__(self, requests: int, window_seconds: int, scope: str = "global"): """ :param requests: number of requests allowed :param window_seconds: window time in seconds :param scope: type of limit ("global" for all site, "endpoint" for a specific path) """ self.requests = requests self.window_seconds = window_seconds self.scope = scope async def __call__(self, request: Request): # getting client ip client_ip = request.client.host if request.client else "127.0.0.1" # when project is in docker and behind nginx, the real ip is in the headers real_ip = request.headers.get("x-real-ip", request.headers.get("x-forwarded-for", client_ip)) # if there are multiple ips, take the first ip (the real user ip) real_ip = real_ip.split(",")[0].strip() # creating redis key based on scope if self.scope == "global": # key for global limit (e.g., rate_limit:global:192.168.1.5) key = f"rate_limit:global:{real_ip}" else: # key for endpoint limit (e.g., rate_limit:endpoint:192.168.1.5:/admin/login) path = request.scope["path"] key = f"rate_limit:endpoint:{real_ip}:{path}" # adding 1 to the number of requests for this ip in redis current_count = await redis_client.incr(key) # if this is the first request in this time window, set the expiration time (TTL) if current_count == 1: await redis_client.expire(key, self.window_seconds) # if the number of requests exceeds the limit, access is blocked if current_count > self.requests: # penalty: if someone spams, the time they are blocked is extended from zero again await redis_client.expire(key, self.window_seconds) raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests. Please try again later." )