Spaces:
Sleeping
Sleeping
| """Phase 15.2 — Rate limiting via slowapi. | |
| Exposes a shared `limiter` plus a request key function that differentiates | |
| authenticated users (keyed by user id) from anonymous clients (keyed by IP). | |
| slowapi 0.1.9 calls `exempt_when()` with no arguments, so we stash the current | |
| Request in a ContextVar via a middleware (`RateLimitContextMiddleware`) and | |
| read from it inside the exempt predicates. | |
| """ | |
| from __future__ import annotations | |
| from contextvars import ContextVar | |
| from fastapi import Request | |
| from slowapi import Limiter | |
| from slowapi.util import get_remote_address | |
| from starlette.middleware.base import BaseHTTPMiddleware | |
| from services.auth_service import decode_token | |
| _REQUEST_STASH: ContextVar[Request | None] = ContextVar("_REQUEST_STASH", default=None) | |
| def _bearer(request: Request) -> str | None: | |
| auth = request.headers.get("authorization") | |
| if not auth: | |
| return None | |
| parts = auth.split() | |
| if len(parts) != 2 or parts[0].lower() != "bearer": | |
| return None | |
| return parts[1] | |
| def _authed_user_id(request: Request) -> str | None: | |
| token = _bearer(request) | |
| if not token: | |
| return None | |
| payload = decode_token(token) | |
| if not payload or "sub" not in payload: | |
| return None | |
| return str(payload["sub"]) | |
| def request_key(request: Request) -> str: | |
| """Keyed on user id when authed, IP address otherwise.""" | |
| uid = _authed_user_id(request) | |
| if uid: | |
| return f"user:{uid}" | |
| return f"ip:{get_remote_address(request)}" | |
| def is_authed() -> bool: | |
| request = _REQUEST_STASH.get() | |
| if request is None: | |
| return False | |
| return _authed_user_id(request) is not None | |
| def is_anon() -> bool: | |
| request = _REQUEST_STASH.get() | |
| if request is None: | |
| return True | |
| return _authed_user_id(request) is None | |
| class RateLimitContextMiddleware(BaseHTTPMiddleware): | |
| """Stashes the incoming Request in a ContextVar so slowapi's no-arg | |
| `exempt_when` predicates can read it.""" | |
| async def dispatch(self, request: Request, call_next): | |
| token = _REQUEST_STASH.set(request) | |
| try: | |
| return await call_next(request) | |
| finally: | |
| _REQUEST_STASH.reset(token) | |
| # Per-route rate limits — anon gets strict caps, authed gets generous quotas. | |
| ANON_ANALYZE = "5/hour" | |
| AUTH_ANALYZE = "50/hour" | |
| ANON_REPORT = "2/hour" | |
| AUTH_REPORT = "20/hour" | |
| ANON_AUTH_REGISTER = "5/hour" | |
| ANON_AUTH_LOGIN = "10/minute" | |
| limiter = Limiter( | |
| key_func=request_key, | |
| default_limits=[], | |
| headers_enabled=True, | |
| enabled=True, | |
| ) | |