Spaces:
Sleeping
Sleeping
| """ | |
| security.py — Shared-secret API key auth and Redis-backed rate limiting. | |
| ShieldYONO has no user accounts yet (the Flutter app is anonymous — see | |
| AppConstants.guardianCallUserId), so this is a single shared-secret gate | |
| between "anyone on the internet" and the API, not per-user auth. It exists | |
| to stop the backend being a completely open APK-upload proxy; it is NOT a | |
| substitute for real auth once accounts exist. | |
| settings.API_KEY ships with a placeholder default so the app keeps working | |
| out of the box — rotate it via the API_KEY env var before any real | |
| deployment, and keep the Flutter build's --dart-define=API_KEY in sync. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from fastapi import Header, HTTPException, Request, status | |
| from app.config import get_settings | |
| logger = logging.getLogger(__name__) | |
| async def require_api_key(x_api_key: str | None = Header(default=None, alias="X-API-Key")) -> None: | |
| settings = get_settings() | |
| if not settings.API_KEY: | |
| # Explicitly disabled by the operator (empty env var) — no gate. | |
| return | |
| if x_api_key != settings.API_KEY: | |
| raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing API key.") | |
| def rate_limit(limit: int = 30, window_seconds: int = 60): | |
| """Dependency factory: per-IP fixed-window rate limit backed by Redis. | |
| Fails open (no limiting) if Redis is unavailable, matching this | |
| codebase's existing "Redis is a best-effort cache, not a hard | |
| dependency" pattern (see main.py lifespan). | |
| """ | |
| async def _dependency(request: Request) -> None: | |
| redis = getattr(request.app.state, "redis", None) | |
| if redis is None: | |
| return | |
| client_ip = request.client.host if request.client else "unknown" | |
| key = f"ratelimit:{request.url.path}:{client_ip}" | |
| try: | |
| count = await redis.incr(key) | |
| if count == 1: | |
| await redis.expire(key, window_seconds) | |
| except Exception as exc: | |
| logger.warning("Rate limiter unavailable, failing open: %s", exc) | |
| return | |
| if count > limit: | |
| raise HTTPException( | |
| status_code=status.HTTP_429_TOO_MANY_REQUESTS, | |
| detail=f"Rate limit exceeded ({limit} requests / {window_seconds}s). Try again shortly.", | |
| ) | |
| return _dependency | |