""" 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