From cbf5e5c926b69349078e36ac1dc88b1d7093f21b Mon Sep 17 00:00:00 2001 From: ntohidi Date: Sat, 27 Jun 2026 18:32:48 +0200 Subject: [PATCH] fix(docker): let dashboard/playground load when auth gate is active (#2037) The AuthGateMiddleware blocked UI static pages with 401 because browsers cannot attach Authorization headers to top-level navigation. The UI shell serves no data, so it is safe to load without credentials. - Add public_prefixes to AuthGateMiddleware for prefix-based path bypass - Register /dashboard, /playground, /static as public prefixes - Add token input bar to both playground and dashboard UIs - Replace all bare fetch() calls with authFetch() that attaches Bearer token - Append ?token= to monitor WebSocket URL (gate already accepts it for WS) All API/data routes remain fail-closed behind the auth gate. Closes #2037 --- deploy/docker/auth_gate.py | 9 ++- deploy/docker/server.py | 1 + deploy/docker/static/monitor/index.html | 77 ++++++++++++++++++---- deploy/docker/static/playground/index.html | 58 ++++++++++++++-- 4 files changed, 125 insertions(+), 20 deletions(-) diff --git a/deploy/docker/auth_gate.py b/deploy/docker/auth_gate.py index 2a037e508..8cb0e4d24 100644 --- a/deploy/docker/auth_gate.py +++ b/deploy/docker/auth_gate.py @@ -15,6 +15,7 @@ * a valid HS256 JWT minted by this server -> the token's own scope claim. Public paths (the health check and the token-issuing endpoint) pass through. +Public prefixes (the UI static shells) also pass through - they serve no data. On failure: HTTP 401 JSON, or WebSocket close 4401. On success: the validated principal is attached at scope["state"]["principal"] (readable downstream as request.state.principal) for scope/ownership checks. @@ -38,10 +39,12 @@ def __init__( *, token_provider: Callable[[], str], public_paths: Iterable[str] = (), + public_prefixes: Iterable[str] = (), ): self.app = app self._token_provider = token_provider self.public_paths = set(public_paths) + self.public_prefixes = tuple(public_prefixes) # ─────────────────────────── ASGI entry ─────────────────────────── async def __call__(self, scope, receive, send): @@ -49,7 +52,11 @@ async def __call__(self, scope, receive, send): await self.app(scope, receive, send) return - if scope.get("path", "") in self.public_paths: + path = scope.get("path", "") + if path in self.public_paths: + await self.app(scope, receive, send) + return + if self.public_prefixes and path.startswith(self.public_prefixes): await self.app(scope, receive, send) return diff --git a/deploy/docker/server.py b/deploy/docker/server.py index 1bc05d833..9cc1e38ca 100644 --- a/deploy/docker/server.py +++ b/deploy/docker/server.py @@ -400,6 +400,7 @@ def _current_api_token() -> str: AuthGateMiddleware, token_provider=_current_api_token, public_paths={HEALTH_PATH, "/token"}, + public_prefixes=_UI_PREFIXES, ) # ── request body-size limit (DoS) ───────────────────────────────────── diff --git a/deploy/docker/static/monitor/index.html b/deploy/docker/static/monitor/index.html index d0ff878a6..a8ec68156 100644 --- a/deploy/docker/static/monitor/index.html +++ b/deploy/docker/static/monitor/index.html @@ -93,6 +93,14 @@

+ +
+ + + + +
@@ -318,6 +326,47 @@

Control Actions