From 94efb00e342fbd0a7e97f3e007c5017f3feb21d2 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 4 Oct 2025 17:55:08 -0600 Subject: [PATCH] Don't redirect non-auth-host /auth/ to auth site but show basic info on current host, and allow logging out. Adds a new host app for this purpose. --- API.md | 6 +- frontend/host/index.html | 12 +++ frontend/src/host/HostApp.vue | 135 ++++++++++++++++++++++++++++++++++ frontend/src/host/main.js | 11 +++ frontend/vite.config.js | 5 +- passkey/fastapi/auth_host.py | 14 +++- passkey/fastapi/mainapp.py | 6 ++ 7 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 frontend/host/index.html create mode 100644 frontend/src/host/HostApp.vue create mode 100644 frontend/src/host/main.js diff --git a/API.md b/API.md index 8939563..1b42355 100644 --- a/API.md +++ b/API.md @@ -14,14 +14,14 @@ Two deployment modes: 2. Dedicated auth host (`--auth-host auth.example.com`) - The specified auth host serves the UI at the root (`/`, `/admin/`, reset tokens, etc.). - - Other (non‑auth) hosts expose only non‑restricted API endpoints; UI is redirected to the auth host. + - Other (non‑auth) hosts show a lightweight account summary at `/` or `/auth/`, while other UI routes still redirect to the auth host. - Restricted endpoints on non‑auth hosts return `404` instead of redirecting. ### Path Mapping When Auth Host Enabled | Purpose | On Auth Host | On Other Hosts (incoming) | Action | |---------|--------------|---------------------------|--------| -| Main UI | `/` | `/auth/` or `/` | Redirect -> auth host `/` (strip leading `/auth` if present) | +| Main UI | `/` | `/auth/` or `/` | Serve account summary SPA (no redirect) | | Admin UI root | `/admin/` | `/auth/admin/` or `/admin/` | Redirect -> auth host `/admin/` (strip `/auth`) | | Reset / device addition token | `/{token}` | `/auth/{token}` | Redirect -> auth host `/{token}` (strip `/auth`) | | Static assets | `/auth/assets/*` | `/auth/assets/*` | Served directly (no redirect) | @@ -38,7 +38,7 @@ Notes: | Method | Path (multi‑host) | Path (auth host) | Description | |--------|-------------------|------------------|-------------| -| GET | `/auth/` | `/` | Main authentication SPA | +| GET | `/auth/` | `/` | Main authentication SPA (non-auth hosts show an account summary view) | | GET | `/auth/admin/` | `/admin/` | Admin SPA root | | GET | `/auth/{reset_token}` | `/{reset_token}` | Reset / device addition SPA (token validated) | | GET | `/auth/restricted` | `/restricted` | Restricted / permission denied SPA | diff --git a/frontend/host/index.html b/frontend/host/index.html new file mode 100644 index 0000000..7c61832 --- /dev/null +++ b/frontend/host/index.html @@ -0,0 +1,12 @@ + + + + + + Account Summary + + +
+ + + diff --git a/frontend/src/host/HostApp.vue b/frontend/src/host/HostApp.vue new file mode 100644 index 0000000..7880983 --- /dev/null +++ b/frontend/src/host/HostApp.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/frontend/src/host/main.js b/frontend/src/host/main.js new file mode 100644 index 0000000..df154cc --- /dev/null +++ b/frontend/src/host/main.js @@ -0,0 +1,11 @@ +import '@/assets/style.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import HostApp from './HostApp.vue' + +const app = createApp(HostApp) + +app.use(createPinia()) + +app.mount('#app') diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 33cd485..e3b29ec 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -33,6 +33,8 @@ export default defineConfig(({ command, mode }) => ({ // Bypass only root SPA entrypoints + static assets so Vite serves them for HMR. // Admin API endpoints (e.g., /auth/admin/orgs) must still hit backend. if (url === '/auth/' || url === '/auth') return '/' + if (url === '/auth/host' || url === '/auth/host/') return '/host/index.html' + if (url === '/host' || url === '/host/') return '/host/index.html' if (url === '/auth/admin' || url === '/auth/admin/') return '/admin/' if (url.startsWith('/auth/assets/')) return url.replace(/^\/auth/, '') if (/^\/auth\/([a-z]+\.){4}[a-z]+\/?$/.test(url)) return '/reset/index.html' @@ -53,7 +55,8 @@ export default defineConfig(({ command, mode }) => ({ index: resolve(__dirname, 'index.html'), admin: resolve(__dirname, 'admin/index.html'), reset: resolve(__dirname, 'reset/index.html'), - restricted: resolve(__dirname, 'restricted/index.html') + restricted: resolve(__dirname, 'restricted/index.html'), + host: resolve(__dirname, 'host/index.html') }, output: {} } diff --git a/passkey/fastapi/auth_host.py b/passkey/fastapi/auth_host.py index 78e903d..6afce51 100644 --- a/passkey/fastapi/auth_host.py +++ b/passkey/fastapi/auth_host.py @@ -8,7 +8,15 @@ from passkey.util import hostutil, passphrase def is_ui_path(path: str) -> bool: """Check if the path is a UI endpoint.""" - ui_paths = {"/", "/admin", "/admin/", "/auth/", "/auth/admin", "/auth/admin/"} + ui_paths = { + "/", + "/admin", + "/admin/", + "/auth", + "/auth/", + "/auth/admin", + "/auth/admin/", + } if path in ui_paths: return True # Treat reset token pages as UI (dynamic). Accept single-segment tokens. @@ -30,6 +38,8 @@ def is_restricted_path(path: str) -> bool: def should_redirect_to_auth_host(path: str) -> bool: """Determine if the request should be redirected to the auth host.""" + if path in {"/", "/auth", "/auth/"}: + return False return is_ui_path(path) or is_restricted_path(path) @@ -47,7 +57,7 @@ def should_redirect_auth_path_to_root(path: str) -> bool: """Check if /auth/ UI path should be redirected to root on auth host.""" if not path.startswith("/auth/"): return False - ui_paths = {"/auth/", "/auth/admin", "/auth/admin/"} + ui_paths = {"/auth", "/auth/", "/auth/admin", "/auth/admin/"} if path in ui_paths: return True # Check for reset token diff --git a/passkey/fastapi/mainapp.py b/passkey/fastapi/mainapp.py index 3878f8f..ea8c129 100644 --- a/passkey/fastapi/mainapp.py +++ b/passkey/fastapi/mainapp.py @@ -76,6 +76,12 @@ async def frontapp( try: await get_session(auth, host=request.headers.get("host")) + cfg_host = hostutil.configured_auth_host() + if cfg_host: + cur_host = hostutil.normalize_host(request.headers.get("host")) + cfg_normalized = hostutil.normalize_host(cfg_host) + if cur_host and cfg_normalized and cur_host != cfg_normalized: + return FileResponse(frontend.file("host", "index.html")) return FileResponse(frontend.file("index.html")) except Exception: if auth: