"""Rate limit на основе Redis `INCR`+`EXPIRE` для публичных (без auth) эндпоинтов. Используется резолвом конференций и гостевым входом (`api/conferences.py`) — эндпоинтами без аутентификации, уязвимыми к перебору номера/ссылки конференции (см. ADR-001, п.4 — оценка энтропии и рекомендуемый лимит 10 запросов/мин на IP). """ from fastapi import HTTPException, status from core.redis import redis_client RATE_LIMIT_MAX_REQUESTS = 10 RATE_LIMIT_WINDOW_SECONDS = 60 async def enforce_rate_limit( key: str, *, max_requests: int = RATE_LIMIT_MAX_REQUESTS, window_seconds: int = RATE_LIMIT_WINDOW_SECONDS, ) -> None: """Увеличить счётчик запросов по ключу; бросить 429, если лимит превышен. `INCR` атомарно создаёт ключ со значением 1, если его ещё не было; TTL выставляется только при первом инкременте в окне (когда счётчик стал равен 1) — иначе окно продлевалось бы при каждом запросе и лимит никогда бы не истекал. """ redis_key = f"rate_limit:{key}" current = await redis_client.incr(redis_key) if current == 1: await redis_client.expire(redis_key, window_seconds) if current > max_requests: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="rate_limit_exceeded" )