Add per-key OpenAPI document and Swagger UI portal

GET /openapi.json (X-API-Key authenticated) merges the upstream OpenAPI
documents into one spec scoped to the calling key: only granted
operations, paths rewritten to gateway routes, component schemas
namespaced per service. GET /docs serves a Swagger UI portal that loads
the key-scoped spec and injects the key into try-it-out requests.

Discovery now caches the raw upstream spec documents (same 5-minute
TTL), and FastAPI's built-in /docs and /openapi.json are disabled in
favor of the portal routes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Amar
2026-07-29 14:27:44 +02:00
co-authored by Claude Fable 5
parent 77d7a50fa9
commit d5878f6130
4 changed files with 231 additions and 2 deletions
+6 -2
View File
@@ -6,7 +6,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
from sqlalchemy import text
from app import config, proxy, retention, security
from app import config, portal, proxy, retention, security
from app.admin import routes as admin_routes
from app.admin import stats as admin_stats
from app.admin.deps import LoginRequired, login_redirect_handler
@@ -111,7 +111,10 @@ async def lifespan(app: FastAPI):
await proxy.close_client()
app = FastAPI(title="API Gateway", version="2.0.0", lifespan=lifespan)
# Built-in docs/openapi are disabled: the gateway serves its own consumer-facing
# /docs and per-key /openapi.json (app/portal.py) at those paths instead.
app = FastAPI(title="API Gateway", version="2.0.0", lifespan=lifespan,
docs_url=None, redoc_url=None, openapi_url=None)
app.add_exception_handler(LoginRequired, login_redirect_handler)
@@ -127,6 +130,7 @@ def health():
app.include_router(admin_routes.router)
app.include_router(admin_stats.router)
app.include_router(portal.router)
app.mount("/static", StaticFiles(directory=str(config.BASE_DIR / "app" / "static")), name="static")
# The proxy catch-all (/{slug}/...) must come last so it never shadows
# /admin, /static, /docs or /health.