Dashboard authenticates: token prompt + bearer on REST/WS/recordings

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>
This commit is contained in:
2026-07-11 06:21:55 -04:00
parent dff21f7d5c
commit e7c84885d9
4 changed files with 100 additions and 17 deletions

29
main.py
View File

@@ -261,9 +261,13 @@ app = FastAPI(
# call_history must register before calls: both live under /api/v1/calls and
# calls' GET /{call_id} would otherwise capture the literal path "history".
_auth = [Depends(require_token)]
app.include_router(call_history.router, prefix="/api/v1/calls", tags=["Call History"], dependencies=_auth)
app.include_router(
call_history.router, prefix="/api/v1/calls", tags=["Call History"], dependencies=_auth
)
app.include_router(calls.router, prefix="/api/v1/calls", tags=["Calls"], dependencies=_auth)
app.include_router(call_flows.router, prefix="/api/v1/call-flows", tags=["Call Flows"], dependencies=_auth)
app.include_router(
call_flows.router, prefix="/api/v1/call-flows", tags=["Call Flows"], dependencies=_auth
)
app.include_router(devices.router, prefix="/api/v1/devices", tags=["Devices"], dependencies=_auth)
app.include_router(routing.router, prefix="/api/v1/routing", tags=["Routing"], dependencies=_auth)
# WebSocket endpoints check the token themselves (query param or header)
@@ -273,6 +277,27 @@ app.include_router(websocket.router, prefix="/ws", tags=["WebSocket"])
app.mount("/mcp", mcp_http_app)
@app.get("/api/v1/status", tags=["System"], dependencies=_auth)
async def api_status():
"""Gateway status summary for the dashboard header."""
gateway = getattr(app.state, "gateway", None)
if gateway:
status = await gateway.status()
return {
"name": "Hold Slayer Gateway",
"version": "0.1.0",
"status": "running",
"uptime": status["uptime"],
"active_calls": status["active_calls"],
"trunk": status["trunk"],
}
return {
"name": "Hold Slayer Gateway",
"version": "0.1.0",
"status": "starting",
}
@app.get("/health", tags=["System"])
async def health():
"""