From adbab88c860ea0f4cf2e55150ce28410d58a7e5d Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Tue, 2 Dec 2025 22:09:07 +0000 Subject: [PATCH] Major refactor of frontend source tree such that paths better match where they are served. --- API.md | 2 +- examples/index.html | 127 ++++++++++++++++-- examples/restricted-api.html | 6 +- frontend/{src => auth}/App.vue | 2 +- frontend/{src => auth}/admin/AdminApp.vue | 10 +- frontend/{ => auth}/admin/index.html | 2 +- frontend/{src => auth}/admin/main.js | 2 +- frontend/{ => auth}/index.html | 2 +- frontend/{src => auth}/main.js | 2 +- .../restricted/RestrictedApi.vue} | 0 frontend/auth/restricted/index.html | 4 + frontend/auth/restricted/main.js | 5 + .../forward/RestrictedForward.vue} | 0 .../{restricted => int}/forward/index.html | 2 +- .../restricted/api => int/forward}/main.js | 2 +- frontend/{src => int}/host/HostApp.vue | 0 frontend/{ => int}/host/index.html | 2 +- frontend/{src => int}/host/main.js | 0 frontend/{src => int}/reset/ResetApp.vue | 0 frontend/{ => int}/reset/index.html | 2 +- frontend/{src => int}/reset/main.js | 0 frontend/restricted/api/index.html | 1 - frontend/src/components/RestrictedAuth.vue | 2 +- frontend/src/restricted/forward/main.js | 5 - frontend/vite.config.js | 113 +++++++++++----- passkey/db/sql.py | 2 +- passkey/fastapi/admin.py | 2 +- passkey/fastapi/api.py | 10 +- passkey/fastapi/mainapp.py | 17 ++- passkey/util/userinfo.py | 5 +- 30 files changed, 243 insertions(+), 86 deletions(-) rename frontend/{src => auth}/App.vue (98%) rename frontend/{src => auth}/admin/AdminApp.vue (98%) rename frontend/{ => auth}/admin/index.html (79%) rename frontend/{src => auth}/admin/main.js (86%) rename frontend/{ => auth}/index.html (81%) rename frontend/{src => auth}/main.js (85%) rename frontend/{src/restricted/api/App.vue => auth/restricted/RestrictedApi.vue} (100%) create mode 100644 frontend/auth/restricted/index.html create mode 100644 frontend/auth/restricted/main.js rename frontend/{src/restricted/forward/App.vue => int/forward/RestrictedForward.vue} (100%) rename frontend/{restricted => int}/forward/index.html (76%) rename frontend/{src/restricted/api => int/forward}/main.js (68%) rename frontend/{src => int}/host/HostApp.vue (100%) rename frontend/{ => int}/host/index.html (80%) rename frontend/{src => int}/host/main.js (100%) rename frontend/{src => int}/reset/ResetApp.vue (100%) rename frontend/{ => int}/reset/index.html (80%) rename frontend/{src => int}/reset/main.js (100%) delete mode 100644 frontend/restricted/api/index.html delete mode 100644 frontend/src/restricted/forward/main.js diff --git a/API.md b/API.md index e6511ad..cce7e90 100644 --- a/API.md +++ b/API.md @@ -48,7 +48,7 @@ Always under `/auth/api/` (even on auth host): | Method | Path | Description | |--------|------|-------------| -| GET | `/auth/api/restricted` | Authentication UI for iframe embedding (supports `?mode=login` or `?mode=reauth`) | +| GET | `/auth/restricted` | Authentication UI for iframe embedding (supports `?mode=login` or `?mode=reauth`) | |--------|------|-------------| | POST | `/auth/api/validate` | Validate & (conditionally) renew session | | GET | `/auth/api/forward` | Auth proxy endpoint for reverse proxies (204 or 4xx) | diff --git a/examples/index.html b/examples/index.html index e13490f..eddd32a 100644 --- a/examples/index.html +++ b/examples/index.html @@ -3,22 +3,123 @@ - PassKey Auth - Examples + PassKey Auth - Dev Mode +
-
-

🔐 PassKey Auth Examples

-

Interactive demos and code examples for PassKey authentication

-
+

🔐 PassKey Auth

+

Dev server running this example application.

-
-
- -

API Demo: login and re-authentication via JS.

-

-

-
+
+

Main Application

+ +
+ + + +
+

Dev Server: Vite on port 4403

+

Backend: localhost:4402

+
-
+ + diff --git a/examples/restricted-api.html b/examples/restricted-api.html index c00096a..759fd2d 100644 --- a/examples/restricted-api.html +++ b/examples/restricted-api.html @@ -111,7 +111,7 @@ iframe.title = 'Authentication'; document.body.appendChild(iframe); } - iframe.src = '/auth/api/restricted?mode=login'; + iframe.src = '/auth/restricted?mode=login'; showStatus('Login mode loaded - for users who are not authenticated', 'info'); } @@ -123,7 +123,7 @@ iframe.title = 'Authentication'; document.body.appendChild(iframe); } - iframe.src = '/auth/api/restricted?mode=reauth'; + iframe.src = '/auth/restricted?mode=reauth'; showStatus('Reauth mode loaded - for additional verification of authenticated users', 'info'); } @@ -133,7 +133,7 @@ iframe = document.createElement('iframe'); iframe.id = 'auth-iframe'; iframe.title = 'Authentication'; - iframe.src = '/auth/api/restricted'; + iframe.src = '/auth/restricted'; document.body.appendChild(iframe); iframeInitialized = true; } diff --git a/frontend/src/App.vue b/frontend/auth/App.vue similarity index 98% rename from frontend/src/App.vue rename to frontend/auth/App.vue index 9ba28fe..54016f7 100644 --- a/frontend/src/App.vue +++ b/frontend/auth/App.vue @@ -57,7 +57,7 @@ function showAuthIframe() { authIframe = document.createElement('iframe') authIframe.id = 'auth-iframe' authIframe.title = 'Authentication' - authIframe.src = '/auth/api/restricted?mode=login' + authIframe.src = '/auth/restricted?mode=login' document.body.appendChild(authIframe) loadingMessage.value = 'Authentication required...' } diff --git a/frontend/src/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue similarity index 98% rename from frontend/src/admin/AdminApp.vue rename to frontend/auth/admin/AdminApp.vue index 020794a..8274d7c 100644 --- a/frontend/src/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -7,10 +7,10 @@ import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue' import StatusMessage from '@/components/StatusMessage.vue' import LoadingView from '@/components/LoadingView.vue' import AuthRequiredMessage from '@/components/AccessDenied.vue' -import AdminOverview from './AdminOverview.vue' -import AdminOrgDetail from './AdminOrgDetail.vue' -import AdminUserDetail from './AdminUserDetail.vue' -import AdminDialogs from './AdminDialogs.vue' +import AdminOverview from '@/admin/AdminOverview.vue' +import AdminOrgDetail from '@/admin/AdminOrgDetail.vue' +import AdminUserDetail from '@/admin/AdminUserDetail.vue' +import AdminDialogs from '@/admin/AdminDialogs.vue' import { useAuthStore } from '@/stores/auth' import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings' @@ -330,7 +330,7 @@ function showAuthIframe() { authIframe = document.createElement('iframe') authIframe.id = 'auth-iframe' authIframe.title = 'Authentication' - authIframe.src = '/auth/api/restricted?mode=login' + authIframe.src = '/auth/restricted?mode=login' document.body.appendChild(authIframe) loadingMessage.value = 'Authentication required...' } diff --git a/frontend/admin/index.html b/frontend/auth/admin/index.html similarity index 79% rename from frontend/admin/index.html rename to frontend/auth/admin/index.html index 8f0596a..5d38a85 100644 --- a/frontend/admin/index.html +++ b/frontend/auth/admin/index.html @@ -7,6 +7,6 @@
- + diff --git a/frontend/src/admin/main.js b/frontend/auth/admin/main.js similarity index 86% rename from frontend/src/admin/main.js rename to frontend/auth/admin/main.js index 9b4a85c..c8848b8 100644 --- a/frontend/src/admin/main.js +++ b/frontend/auth/admin/main.js @@ -1,4 +1,4 @@ -import '../assets/style.css' +import '@/assets/style.css' import { createApp } from 'vue' import { createPinia } from 'pinia' diff --git a/frontend/index.html b/frontend/auth/index.html similarity index 81% rename from frontend/index.html rename to frontend/auth/index.html index a8c4221..3e90ccb 100644 --- a/frontend/index.html +++ b/frontend/auth/index.html @@ -7,6 +7,6 @@
- + diff --git a/frontend/src/main.js b/frontend/auth/main.js similarity index 85% rename from frontend/src/main.js rename to frontend/auth/main.js index cf77201..231bb07 100644 --- a/frontend/src/main.js +++ b/frontend/auth/main.js @@ -1,4 +1,4 @@ -import './assets/style.css' +import '@/assets/style.css' import { createApp } from 'vue' import { createPinia } from 'pinia' diff --git a/frontend/src/restricted/api/App.vue b/frontend/auth/restricted/RestrictedApi.vue similarity index 100% rename from frontend/src/restricted/api/App.vue rename to frontend/auth/restricted/RestrictedApi.vue diff --git a/frontend/auth/restricted/index.html b/frontend/auth/restricted/index.html new file mode 100644 index 0000000..f06d835 --- /dev/null +++ b/frontend/auth/restricted/index.html @@ -0,0 +1,4 @@ + + +
+ diff --git a/frontend/auth/restricted/main.js b/frontend/auth/restricted/main.js new file mode 100644 index 0000000..ed6ba37 --- /dev/null +++ b/frontend/auth/restricted/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import RestrictedApi from './RestrictedApi.vue' +import '@/assets/style.css' + +createApp(RestrictedApi).mount('#app') diff --git a/frontend/src/restricted/forward/App.vue b/frontend/int/forward/RestrictedForward.vue similarity index 100% rename from frontend/src/restricted/forward/App.vue rename to frontend/int/forward/RestrictedForward.vue diff --git a/frontend/restricted/forward/index.html b/frontend/int/forward/index.html similarity index 76% rename from frontend/restricted/forward/index.html rename to frontend/int/forward/index.html index a51815d..c7c689c 100644 --- a/frontend/restricted/forward/index.html +++ b/frontend/int/forward/index.html @@ -7,6 +7,6 @@
- + diff --git a/frontend/src/restricted/api/main.js b/frontend/int/forward/main.js similarity index 68% rename from frontend/src/restricted/api/main.js rename to frontend/int/forward/main.js index c16e86a..49f5891 100644 --- a/frontend/src/restricted/api/main.js +++ b/frontend/int/forward/main.js @@ -1,5 +1,5 @@ import { createApp } from 'vue' -import App from './App.vue' +import App from './RestrictedForward.vue' import '@/assets/style.css' createApp(App).mount('#app') diff --git a/frontend/src/host/HostApp.vue b/frontend/int/host/HostApp.vue similarity index 100% rename from frontend/src/host/HostApp.vue rename to frontend/int/host/HostApp.vue diff --git a/frontend/host/index.html b/frontend/int/host/index.html similarity index 80% rename from frontend/host/index.html rename to frontend/int/host/index.html index 7c61832..f739627 100644 --- a/frontend/host/index.html +++ b/frontend/int/host/index.html @@ -7,6 +7,6 @@
- + diff --git a/frontend/src/host/main.js b/frontend/int/host/main.js similarity index 100% rename from frontend/src/host/main.js rename to frontend/int/host/main.js diff --git a/frontend/src/reset/ResetApp.vue b/frontend/int/reset/ResetApp.vue similarity index 100% rename from frontend/src/reset/ResetApp.vue rename to frontend/int/reset/ResetApp.vue diff --git a/frontend/reset/index.html b/frontend/int/reset/index.html similarity index 80% rename from frontend/reset/index.html rename to frontend/int/reset/index.html index 12122ab..98a17f4 100644 --- a/frontend/reset/index.html +++ b/frontend/int/reset/index.html @@ -7,6 +7,6 @@
- + diff --git a/frontend/src/reset/main.js b/frontend/int/reset/main.js similarity index 100% rename from frontend/src/reset/main.js rename to frontend/int/reset/main.js diff --git a/frontend/restricted/api/index.html b/frontend/restricted/api/index.html deleted file mode 100644 index 9c80eaa..0000000 --- a/frontend/restricted/api/index.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/frontend/src/components/RestrictedAuth.vue b/frontend/src/components/RestrictedAuth.vue index 00e9688..d3c355e 100644 --- a/frontend/src/components/RestrictedAuth.vue +++ b/frontend/src/components/RestrictedAuth.vue @@ -85,7 +85,7 @@ const headerMessage = computed(() => { if (props.mode === 'reauth') { return 'Please verify your identity to continue with this action.' } - return currentView.value === 'forbidden' ? 'You lack the required permissions.' : 'Please sign in with your Passkey.' + return currentView.value === 'forbidden' ? 'You lack the required permissions.' : 'Please sign in with your passkey.' }) const userDisplayName = computed(() => userInfo.value?.user?.user_name || 'User') diff --git a/frontend/src/restricted/forward/main.js b/frontend/src/restricted/forward/main.js deleted file mode 100644 index c16e86a..0000000 --- a/frontend/src/restricted/forward/main.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createApp } from 'vue' -import App from './App.vue' -import '@/assets/style.css' - -createApp(App).mount('#app') diff --git a/frontend/vite.config.js b/frontend/vite.config.js index f72fa30..61c8175 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -2,52 +2,92 @@ import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import { resolve } from 'node:path' import vue from '@vitejs/plugin-vue' -import { readFileSync, existsSync, statSync } from 'node:fs' +import { readFileSync, existsSync, statSync, renameSync, mkdirSync } from 'node:fs' export default defineConfig(({ command }) => ({ appType: 'mpa', + publicDir: 'public', plugins: [ vue(), { - name: 'serve-examples', - configureServer(server) { - server.middlewares.use((req, res, next) => { - const url = req.url?.split('?')[0] - if (url === '/examples') return res.writeHead(301, { Location: '/examples/' }).end() - if (url?.startsWith('/examples/')) { - const file = resolve(__dirname, '../examples', url === '/examples/' ? 'index.html' : url.slice(10)) - if (existsSync(file) && statSync(file).isFile()) { - res.setHeader('Content-Type', { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript' }[file.slice(file.lastIndexOf('.'))] || 'text/plain') - return res.end(readFileSync(file)) - } - return res.writeHead(404).end() + name: 'move-html-files', + closeBundle() { + if (command !== 'build') return + + const outDir = resolve(__dirname, '../passkey/frontend-build') + const moves = [ + { from: 'auth.html', to: 'auth/index.html' }, + { from: 'admin.html', to: 'admin/index.html' }, + { from: 'restricted.html', to: 'restricted/index.html' }, + { from: 'host.html', to: 'host/index.html' }, + { from: 'reset.html', to: 'reset/index.html' }, + { from: 'forward.html', to: 'forward/index.html' } + ] + + for (const { from, to } of moves) { + const fromPath = resolve(outDir, from) + const toPath = resolve(outDir, to) + if (existsSync(fromPath)) { + mkdirSync(resolve(outDir, to.split('/')[0]), { recursive: true }) + renameSync(fromPath, toPath) } - next() - }) + } } } ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, - base: command === 'build' ? '/auth/' : '/', + base: '/', server: { port: 4403, + fs: { + allow: ['..'] + }, proxy: { + '/': { + target: 'http://localhost:4402', + bypass: (req) => { + const url = req.url?.split('?')[0] + + // Root and examples served by Vite + if (url === '/' || url === '') return '/../examples/index.html' + if (url === '/examples' || url === '/examples/') return '/../examples/index.html' + if (url?.startsWith('/examples/')) return `/../examples${url.slice(9)}` + + // Let other proxies handle their routes + return null + } + }, '/auth/': { target: 'http://localhost:4402', ws: true, bypass: (req) => { const url = req.url?.split('?')[0] - if (url?.startsWith('/auth/assets/')) return url.slice(5) - if (url === '/auth/api/restricted') return '/restricted/api/index.html' - - const routes = { '': '/', host: '/host/index.html', admin: '/admin/', 'restricted-forward': '/restricted/forward/index.html', 'restricted-api': '/restricted/api/index.html' } - for (const [path, target] of Object.entries(routes)) { - if ([`/auth/${path}`, `/auth/${path}/`, `/${path}`, `/${path}/`].includes(url)) return target - } - - if (/^\/auth\/([a-z]+\.){4}[a-z]+\/?$|^\/([a-z]+\.){4}[a-z]+\/?$/.test(url)) return '/reset/index.html' + // Backend handles /auth/api/* and /auth/ws/* (no bypass - let proxy handle) + if (url?.startsWith('/auth/api/') || url?.startsWith('/auth/ws/')) return null + // Vite serves all assets + if (url?.startsWith('/auth/assets/')) return url + // Vite serves main app routes + if (url === '/auth' || url === '/auth/') return '/auth/index.html' + if (url === '/auth/admin' || url === '/auth/admin/') return '/auth/admin/index.html' + if (url === '/auth/restricted' || url === '/auth/restricted/') return '/auth/restricted/index.html' + if (url?.startsWith('/auth/') && /^\/auth\/([a-z]+\.)+[a-z]+\/?/.test(url)) return "/int/reset/index.html" + // Vite serves source files (for HMR and dev) + if (url?.startsWith('/auth/') && /\.(js|vue|css|ts|jsx|tsx|json)$/.test(url)) return url + return null + } + }, + '/int/': { + target: 'http://localhost:4402', + bypass: (req) => { + const url = req.url?.split('?')[0] + // Vite serves /int/ apps + if (url === '/int/host' || url === '/int/host/') return '/int/host/index.html' + if (url === '/int/reset' || url === '/int/reset/' || url?.match(/^\/int\/reset\/([a-z]+\.){4}[a-z]+\/?$/)) return '/int/reset/index.html' + if (url === '/int/forward' || url === '/int/forward/') return '/int/forward/index.html' + if (url?.startsWith('/int/')) return url + return null } } } @@ -57,12 +97,23 @@ export default defineConfig(({ command }) => ({ emptyOutDir: true, rollupOptions: { input: { - index: resolve(__dirname, 'index.html'), - admin: resolve(__dirname, 'admin/index.html'), - reset: resolve(__dirname, 'reset/index.html'), - 'restricted-forward': resolve(__dirname, 'restricted/forward/index.html'), - 'restricted-api': resolve(__dirname, 'restricted/api/index.html'), - host: resolve(__dirname, 'host/index.html') + auth: resolve(__dirname, 'auth/index.html'), + admin: resolve(__dirname, 'auth/admin/index.html'), + restricted: resolve(__dirname, 'auth/restricted/index.html'), + host: resolve(__dirname, 'int/host/index.html'), + reset: resolve(__dirname, 'int/reset/index.html'), + forward: resolve(__dirname, 'int/forward/index.html') + }, + output: { + entryFileNames: (chunkInfo) => { + return 'auth/assets/[name]-[hash].js' + }, + chunkFileNames: (chunkInfo) => { + return 'auth/assets/[name]-[hash].js' + }, + assetFileNames: (assetInfo) => { + return 'auth/assets/[name]-[hash][extname]' + } } } } diff --git a/passkey/db/sql.py b/passkey/db/sql.py index f2b9cc0..31cff7b 100644 --- a/passkey/db/sql.py +++ b/passkey/db/sql.py @@ -439,7 +439,7 @@ class DB(DatabaseInterface): credential_model = result.scalar_one_or_none() if not credential_model: - raise ValueError("This passkey is not registered with this service") + raise ValueError("Credential not found") return Credential( uuid=UUID(bytes=credential_model.uuid), credential_id=credential_model.credential_id, diff --git a/passkey/fastapi/admin.py b/passkey/fastapi/admin.py index 9b8edcb..ab78de4 100644 --- a/passkey/fastapi/admin.py +++ b/passkey/fastapi/admin.py @@ -36,7 +36,7 @@ async def general_exception_handler(_request, exc: Exception): @app.get("/") async def adminapp(request: Request, auth=AUTH_COOKIE): - return FileResponse(frontend.file("admin/index.html")) + return FileResponse(frontend.file("auth", "admin", "index.html")) # -------------------- Organizations -------------------- diff --git a/passkey/fastapi/api.py b/passkey/fastapi/api.py index 87ee224..6e502ec 100644 --- a/passkey/fastapi/api.py +++ b/passkey/fastapi/api.py @@ -10,7 +10,7 @@ from fastapi import ( Request, Response, ) -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import JSONResponse from fastapi.security import HTTPBearer from passkey.util import frontend @@ -36,12 +36,6 @@ app = FastAPI() app.mount("/user", user.app) -@app.get("/restricted") -async def restricted_view(): - """Serve the restricted/authentication UI for iframe embedding.""" - return FileResponse(frontend.file("restricted/api", "index.html")) - - @app.exception_handler(HTTPException) async def http_exception_handler(_request: Request, exc: HTTPException): """Ensure auth cookie is cleared on 401 responses (JSON responses only).""" @@ -155,7 +149,7 @@ async def forward_authentication( return Response(status_code=204, headers=remote_headers) except HTTPException as e: # Let global handler clear cookie; still return HTML surface instead of JSON - html = frontend.file("restricted/forward", "index.html").read_bytes() + html = frontend.file("int", "restricted", "index.html").read_bytes() status = e.status_code # If 401 we still want cookie cleared; rely on handler by raising again not feasible (we need HTML) if status == 401: diff --git a/passkey/fastapi/mainapp.py b/passkey/fastapi/mainapp.py index a6f888e..dbf18a4 100644 --- a/passkey/fastapi/mainapp.py +++ b/passkey/fastapi/mainapp.py @@ -55,9 +55,18 @@ app.mount("/auth/admin/", admin.app) app.mount("/auth/api/", api.app) app.mount("/auth/ws/", ws.app) app.mount( - "/auth/assets/", StaticFiles(directory=frontend.file("assets")), name="assets" + "/auth/assets/", + StaticFiles(directory=frontend.file("auth", "assets")), + name="assets", ) + +@app.get("/auth/restricted") +async def restricted_view(): + """Serve the restricted/authentication UI for iframe embedding.""" + return FileResponse(frontend.file("auth", "restricted", "index.html")) + + # Navigable URLs are defined here. We support both / and /auth/ as the base path # / is used on a dedicated auth site, /auth/ on app domains with auth @@ -74,8 +83,8 @@ async def frontapp(request: Request, response: Response, auth=AUTH_COOKIE): 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")) + return FileResponse(frontend.file("int", "host", "index.html")) + return FileResponse(frontend.file("auth", "index.html")) @app.get("/admin", include_in_schema=False) @@ -96,4 +105,4 @@ async def reset_link(reset: str): """Serve the reset app directly with an injected reset token.""" if not passphrase.is_well_formed(reset): raise HTTPException(status_code=404) - return FileResponse(frontend.file("reset", "index.html")) + return FileResponse(frontend.file("int", "reset", "index.html")) diff --git a/passkey/util/userinfo.py b/passkey/util/userinfo.py index f3ec6ef..72eda17 100644 --- a/passkey/util/userinfo.py +++ b/passkey/util/userinfo.py @@ -7,8 +7,7 @@ from passkey.util import useragent from .. import aaguid from ..authsession import session_key from ..globals import db -from ..util import hostutil, permutil -from ..util.tokens import encode_session_key +from . import hostutil, permutil, tokens def _format_datetime(dt): @@ -108,7 +107,7 @@ async def format_user_info( for entry in session_records: sessions_payload.append( { - "id": encode_session_key(entry.key), + "id": tokens.encode_session_key(entry.key), "host": entry.host, "ip": entry.ip, "user_agent": useragent.compact_user_agent(entry.user_agent),