Initial commit: API gateway with admin console

- FastAPI async gateway with httpx proxying to multiple upstreams
- SQLite database with SQLAlchemy ORM
- Admin console: manage services, users, API keys, endpoint access
- Per-key, per-endpoint granular access control
- OpenAPI document sync and caching (5-minute TTL)
- Request/response logging with full transaction inspection
- In-memory rate limiting (per-key, fixed-window)
- Tiered log retention (7d payloads, 90d rows, incremental vacuum)
- TLS verification toggle per service (for self-signed certificates)
- Service connectivity validation with automatic endpoint refresh
- Request browser with filters and deep-link inspection
- Docker setup with persistent volume
- Modal forms for create/edit flows

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Amar
2026-07-29 14:00:22 +02:00
co-authored by Claude Haiku 4.5
commit 77d7a50fa9
31 changed files with 2927 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
"""Keep service endpoint catalogs in sync with the upstream's OpenAPI document."""
import time
import httpx
from sqlalchemy.orm import Session
from app.models import Endpoint, Service
METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
SPEC_PATHS = ("/openapi.json", "/swagger.json", "/api-docs")
# Syncing hits the upstream over HTTP, so results are cached per service and
# only refreshed after the TTL (or on demand with force=True).
SYNC_TTL_SECONDS = 300
_last_sync: dict[int, float] = {} # service id -> monotonic time of last attempt
_spec_found: dict[int, bool] = {} # service id -> did the last attempt find a spec
def fetch_spec(base_url: str, verify_tls: bool = True) -> dict | None:
for candidate in SPEC_PATHS:
try:
resp = httpx.get(base_url.rstrip("/") + candidate, timeout=2,
follow_redirects=True, verify=verify_tls)
if resp.status_code == 200:
data = resp.json()
if isinstance(data, dict) and "paths" in data:
return data
except (httpx.HTTPError, ValueError):
continue
return None
def spec_status(service_id: int) -> bool | None:
"""Did the last sync attempt find an OpenAPI document? None = never tried."""
return _spec_found.get(service_id)
def sync_service(db: Session, service: Service, force: bool = False) -> tuple[bool, bool]:
"""Mirror the upstream OpenAPI paths into the endpoint catalog: add new
operations, refresh descriptions, delete operations that disappeared
(their key grants go with them). Services without a reachable spec are
left untouched. Returns (spec_found, catalog_changed); results are cached
between syncs."""
now = time.monotonic()
if not force and now - _last_sync.get(service.id, float("-inf")) < SYNC_TTL_SECONDS:
return _spec_found.get(service.id, False), False
_last_sync[service.id] = now
spec = fetch_spec(service.base_url, service.verify_tls)
_spec_found[service.id] = spec is not None
if spec is None:
return False, False
return True, apply_spec(db, service, spec)
def validate_service(db: Session, service: Service) -> dict:
"""Connectivity probe for the admin UI. Prefers the OpenAPI document —
finding one both proves reachability and refreshes the endpoint cache —
and falls back to a plain GET on the base URL otherwise."""
start = time.perf_counter()
spec = fetch_spec(service.base_url, service.verify_tls)
if spec is not None:
_last_sync[service.id] = time.monotonic()
_spec_found[service.id] = True
apply_spec(db, service, spec)
return {"ok": True, "spec_found": True,
"endpoints": len(service.endpoints),
"latency_ms": round((time.perf_counter() - start) * 1000, 1)}
try:
resp = httpx.get(service.base_url, timeout=5, follow_redirects=True,
verify=service.verify_tls)
return {"ok": True, "spec_found": False, "status_code": resp.status_code,
"endpoints": len(service.endpoints),
"latency_ms": round((time.perf_counter() - start) * 1000, 1)}
except httpx.HTTPError as exc:
return {"ok": False, "spec_found": False,
"error": str(exc) or type(exc).__name__,
"latency_ms": round((time.perf_counter() - start) * 1000, 1)}
def apply_spec(db: Session, service: Service, spec: dict) -> bool:
"""Write an OpenAPI document's paths into the endpoint catalog.
Returns whether anything changed."""
wanted: dict[tuple[str, str], str] = {}
for path, operations in spec["paths"].items():
if not isinstance(operations, dict):
continue
for method, op in operations.items():
method = method.upper()
if method not in METHODS:
continue
summary = op.get("summary", "") if isinstance(op, dict) else ""
wanted[(method, path)] = summary
existing = {(e.method, e.path): e for e in service.endpoints}
changed = False
for (method, path), summary in wanted.items():
endpoint = existing.get((method, path))
if endpoint is None:
db.add(Endpoint(service_id=service.id, method=method, path=path,
description=summary))
changed = True
elif endpoint.description != summary:
endpoint.description = summary
changed = True
for key, endpoint in existing.items():
if key not in wanted:
db.delete(endpoint)
changed = True
db.commit()
db.expire(service, ["endpoints"])
return changed