The UI never sent the bearer token, so with API_TOKEN set every data call 401'd and /ws/events was rejected pre-accept (the 403s in the uvicorn log). The API client now keeps the token in localStorage, attaches Authorization to every request, prompts once on a 401 and retries, and appends ?token= to the WebSocket connect — the page's reconnect loop picks the token up after the first prompt. require_token also accepts a ?token= query parameter (same convention as the WebSocket) because <audio> elements fetching recordings can't set headers; recordingUrl() rides the token there. The dashboard header's status call moved to a new authenticated GET /api/v1/status — its old source was the JSON root endpoint that the dashboard itself replaced at /. Two new auth tests (query-param accepted / wrong query-param 401); dashboard rebuilt. Verified live: WS rejected without token and connected with ?token=, status 200, ?token=wrong 401. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
55 lines
1.7 KiB
Python
55 lines
1.7 KiB
Python
"""
|
|
API Dependencies — Shared dependency injection for all routes.
|
|
"""
|
|
|
|
import secrets
|
|
|
|
from fastapi import Header, HTTPException, Query, Request
|
|
|
|
from config import get_settings
|
|
from core.gateway import AIPSTNGateway
|
|
|
|
|
|
def get_gateway(request: Request) -> AIPSTNGateway:
|
|
"""Get the gateway instance from app state."""
|
|
gateway = getattr(request.app.state, "gateway", None)
|
|
if gateway is None:
|
|
raise HTTPException(status_code=503, detail="Gateway not initialized")
|
|
return gateway
|
|
|
|
|
|
def get_routing_service(request: Request):
|
|
"""Get the routing service from app state."""
|
|
routing = getattr(request.app.state, "routing_service", None)
|
|
if routing is None:
|
|
raise HTTPException(status_code=503, detail="Routing service not ready")
|
|
return routing
|
|
|
|
|
|
def require_token(
|
|
authorization: str | None = Header(default=None),
|
|
token: str | None = Query(default=None),
|
|
) -> None:
|
|
"""
|
|
Enforce the static bearer token (API_TOKEN) on REST routes.
|
|
|
|
A `token` query parameter is accepted alongside the Authorization
|
|
header for clients that can't set headers — <audio>/<a> elements
|
|
fetching recordings — matching the WebSocket convention.
|
|
|
|
An empty configured token disables auth; startup refuses that
|
|
combination unless the server is bound to loopback.
|
|
"""
|
|
expected = get_settings().api_token.get_secret_value()
|
|
if not expected:
|
|
return
|
|
supplied = token or ""
|
|
if authorization and authorization.lower().startswith("bearer "):
|
|
supplied = authorization[7:]
|
|
if not secrets.compare_digest(supplied, expected):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Missing or invalid bearer token",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|