Spaces:
Running
Running
File size: 4,658 Bytes
a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 f1fa34c a32aee9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | """
API-layer configuration.
Only concerns the HTTP wrapper itself (auth, CORS, docs, limits). All
RAG/model/embedding configuration continues to live in ``app.config.Settings``
and is read from there β this module never duplicates it.
Fails closed: in a non-development environment the app refuses to start without
an explicit JWT secret and CORS allowlist, rather than silently using insecure
defaults.
"""
from __future__ import annotations
import secrets
from functools import lru_cache
from typing import Literal
from pydantic_settings import BaseSettings
Environment = Literal["development", "staging", "production"]
class ConfigurationError(RuntimeError):
"""Raised at startup when required production settings are missing."""
class ApiSettings(BaseSettings):
"""Settings specific to the FastAPI wrapper (read from the same .env)."""
# Which environment we are running in. Anything other than "development"
# enforces the production guardrails below.
environment: Environment = "development"
# ββ Auth βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# MUST be set outside development. Generate with:
# python -c "import secrets; print(secrets.token_urlsafe(48))"
jwt_secret: str = ""
access_token_ttl_minutes: int = 30
refresh_token_ttl_days: int = 30
# ββ CORS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Comma-separated browser origins. Wildcard is never permitted: this API is
# state-changing (it can delete a user's whole library).
# CORS_ORIGINS=https://research-rag.vercel.app,https://researchrag.vercel.app
cors_origins: str = ""
# ββ Docs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# /docs advertises the full attack surface; off by default in production.
expose_docs: bool = False
api_title: str = "ResearchRAG API"
api_version: str = "1.0.0"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
extra = "ignore"
# ββ Derived ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@property
def is_production(self) -> bool:
return self.environment != "development"
@property
def cors_origins_list(self) -> list[str]:
"""Explicit origin allowlist. Never returns ``*``."""
origins = [o.strip() for o in (self.cors_origins or "").split(",") if o.strip()]
origins = [o for o in origins if o != "*"]
if origins:
return origins
if self.is_production:
raise ConfigurationError(
"CORS_ORIGINS must list explicit https origins in "
f"{self.environment} (wildcards are rejected)."
)
# Development convenience only β the local Vite dev server.
return ["http://localhost:5173", "http://127.0.0.1:5173"]
@property
def docs_url(self) -> str | None:
return "/docs" if (self.expose_docs or not self.is_production) else None
@property
def openapi_url(self) -> str | None:
return "/openapi.json" if (self.expose_docs or not self.is_production) else None
def validate_runtime(self) -> None:
"""
Fail fast on missing production configuration.
Called once at startup. In development a random secret is generated so
local work needs no setup β but that secret dies with the process, which
is exactly why it must never be relied on in production.
"""
if self.is_production:
if not self.jwt_secret or len(self.jwt_secret) < 32:
raise ConfigurationError(
"JWT_SECRET must be set to at least 32 characters in "
f"{self.environment}. Generate one with: "
'python -c "import secrets; print(secrets.token_urlsafe(48))"'
)
self.cors_origins_list # raises when unset
elif not self.jwt_secret:
object.__setattr__(self, "jwt_secret", secrets.token_urlsafe(48))
@lru_cache
def get_api_settings() -> ApiSettings:
return ApiSettings()
|