Major refactor of frontend source tree such that paths better match where they are served.
This commit is contained in:
@@ -48,7 +48,7 @@ Always under `/auth/api/` (even on auth host):
|
|||||||
|
|
||||||
| Method | Path | Description |
|
| 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 |
|
| POST | `/auth/api/validate` | Validate & (conditionally) renew session |
|
||||||
| GET | `/auth/api/forward` | Auth proxy endpoint for reverse proxies (204 or 4xx) |
|
| GET | `/auth/api/forward` | Auth proxy endpoint for reverse proxies (204 or 4xx) |
|
||||||
|
|||||||
+114
-13
@@ -3,22 +3,123 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PassKey Auth - Examples</title>
|
<title>PassKey Auth - Dev Mode</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
.section:first-of-type h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.links.side-by-side {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.links.side-by-side a {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
.example-description {
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.info p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<h1>🔐 PassKey Auth</h1>
|
||||||
<h1>🔐 PassKey Auth Examples</h1>
|
<p class="subtitle">Dev server running this example application.</p>
|
||||||
<p class="subtitle">Interactive demos and code examples for PassKey authentication</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="content">
|
<div class="section">
|
||||||
<div class="examples-grid">
|
<h2>Main Application</h2>
|
||||||
<a href="/examples/restricted-api.html" class="example-card">
|
<div class="links side-by-side">
|
||||||
<h3 class="example-title">API Demo: login and re-authentication via JS.</h3>
|
<a href="/auth/">👤 User Profile</a>
|
||||||
<p class="example-description">
|
<a href="/auth/admin/">⚙️ Admin Panel</a>
|
||||||
</p>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Examples & Demos</h2>
|
||||||
|
<div class="links">
|
||||||
|
<a href="/examples/restricted-api.html">
|
||||||
|
📡 API Demo
|
||||||
|
<div class="example-description">Login and re-authentication via JS</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<p><strong>Dev Server:</strong> Vite on port 4403</p>
|
||||||
|
<p><strong>Backend:</strong> localhost:4402</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
iframe.title = 'Authentication';
|
iframe.title = 'Authentication';
|
||||||
document.body.appendChild(iframe);
|
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');
|
showStatus('Login mode loaded - for users who are not authenticated', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
iframe.title = 'Authentication';
|
iframe.title = 'Authentication';
|
||||||
document.body.appendChild(iframe);
|
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');
|
showStatus('Reauth mode loaded - for additional verification of authenticated users', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
iframe = document.createElement('iframe');
|
iframe = document.createElement('iframe');
|
||||||
iframe.id = 'auth-iframe';
|
iframe.id = 'auth-iframe';
|
||||||
iframe.title = 'Authentication';
|
iframe.title = 'Authentication';
|
||||||
iframe.src = '/auth/api/restricted';
|
iframe.src = '/auth/restricted';
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
iframeInitialized = true;
|
iframeInitialized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function showAuthIframe() {
|
|||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.src = '/auth/api/restricted?mode=login'
|
authIframe.src = '/auth/restricted?mode=login'
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
loadingMessage.value = 'Authentication required...'
|
loadingMessage.value = 'Authentication required...'
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,10 @@ import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
|||||||
import StatusMessage from '@/components/StatusMessage.vue'
|
import StatusMessage from '@/components/StatusMessage.vue'
|
||||||
import LoadingView from '@/components/LoadingView.vue'
|
import LoadingView from '@/components/LoadingView.vue'
|
||||||
import AuthRequiredMessage from '@/components/AccessDenied.vue'
|
import AuthRequiredMessage from '@/components/AccessDenied.vue'
|
||||||
import AdminOverview from './AdminOverview.vue'
|
import AdminOverview from '@/admin/AdminOverview.vue'
|
||||||
import AdminOrgDetail from './AdminOrgDetail.vue'
|
import AdminOrgDetail from '@/admin/AdminOrgDetail.vue'
|
||||||
import AdminUserDetail from './AdminUserDetail.vue'
|
import AdminUserDetail from '@/admin/AdminUserDetail.vue'
|
||||||
import AdminDialogs from './AdminDialogs.vue'
|
import AdminDialogs from '@/admin/AdminDialogs.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
|
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ function showAuthIframe() {
|
|||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.src = '/auth/api/restricted?mode=login'
|
authIframe.src = '/auth/restricted?mode=login'
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
loadingMessage.value = 'Authentication required...'
|
loadingMessage.value = 'Authentication required...'
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="admin-app"></div>
|
<div id="admin-app"></div>
|
||||||
<script type="module" src="/src/admin/main.js"></script>
|
<script type="module" src="./main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import '../assets/style.css'
|
import '@/assets/style.css'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import './assets/style.css'
|
import '@/assets/style.css'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<html style="background: transparent"><meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/auth/restricted/main.js"></script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import RestrictedApi from './RestrictedApi.vue'
|
||||||
|
import '@/assets/style.css'
|
||||||
|
|
||||||
|
createApp(RestrictedApi).mount('#app')
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/restricted/forward/main.js"></script>
|
<script type="module" src="/int/forward/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './RestrictedForward.vue'
|
||||||
import '@/assets/style.css'
|
import '@/assets/style.css'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/host/main.js"></script>
|
<script type="module" src="/int/host/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/reset/main.js"></script>
|
<script type="module" src="/int/reset/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html style="background: transparent"><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><div id="app"></div><script type="module" src="/src/restricted/api/main.js"></script>
|
|
||||||
@@ -85,7 +85,7 @@ const headerMessage = computed(() => {
|
|||||||
if (props.mode === 'reauth') {
|
if (props.mode === 'reauth') {
|
||||||
return 'Please verify your identity to continue with this action.'
|
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')
|
const userDisplayName = computed(() => userInfo.value?.user?.user_name || 'User')
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
import '@/assets/style.css'
|
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
|
||||||
+82
-31
@@ -2,52 +2,92 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from 'node:path'
|
||||||
import vue from '@vitejs/plugin-vue'
|
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 }) => ({
|
export default defineConfig(({ command }) => ({
|
||||||
appType: 'mpa',
|
appType: 'mpa',
|
||||||
|
publicDir: 'public',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
{
|
{
|
||||||
name: 'serve-examples',
|
name: 'move-html-files',
|
||||||
configureServer(server) {
|
closeBundle() {
|
||||||
server.middlewares.use((req, res, next) => {
|
if (command !== 'build') return
|
||||||
const url = req.url?.split('?')[0]
|
|
||||||
if (url === '/examples') return res.writeHead(301, { Location: '/examples/' }).end()
|
const outDir = resolve(__dirname, '../passkey/frontend-build')
|
||||||
if (url?.startsWith('/examples/')) {
|
const moves = [
|
||||||
const file = resolve(__dirname, '../examples', url === '/examples/' ? 'index.html' : url.slice(10))
|
{ from: 'auth.html', to: 'auth/index.html' },
|
||||||
if (existsSync(file) && statSync(file).isFile()) {
|
{ from: 'admin.html', to: 'admin/index.html' },
|
||||||
res.setHeader('Content-Type', { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript' }[file.slice(file.lastIndexOf('.'))] || 'text/plain')
|
{ from: 'restricted.html', to: 'restricted/index.html' },
|
||||||
return res.end(readFileSync(file))
|
{ from: 'host.html', to: 'host/index.html' },
|
||||||
}
|
{ from: 'reset.html', to: 'reset/index.html' },
|
||||||
return res.writeHead(404).end()
|
{ 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: {
|
resolve: {
|
||||||
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }
|
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }
|
||||||
},
|
},
|
||||||
base: command === 'build' ? '/auth/' : '/',
|
base: '/',
|
||||||
server: {
|
server: {
|
||||||
port: 4403,
|
port: 4403,
|
||||||
|
fs: {
|
||||||
|
allow: ['..']
|
||||||
|
},
|
||||||
proxy: {
|
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/': {
|
'/auth/': {
|
||||||
target: 'http://localhost:4402',
|
target: 'http://localhost:4402',
|
||||||
ws: true,
|
ws: true,
|
||||||
bypass: (req) => {
|
bypass: (req) => {
|
||||||
const url = req.url?.split('?')[0]
|
const url = req.url?.split('?')[0]
|
||||||
if (url?.startsWith('/auth/assets/')) return url.slice(5)
|
// Backend handles /auth/api/* and /auth/ws/* (no bypass - let proxy handle)
|
||||||
if (url === '/auth/api/restricted') return '/restricted/api/index.html'
|
if (url?.startsWith('/auth/api/') || url?.startsWith('/auth/ws/')) return null
|
||||||
|
// Vite serves all assets
|
||||||
const routes = { '': '/', host: '/host/index.html', admin: '/admin/', 'restricted-forward': '/restricted/forward/index.html', 'restricted-api': '/restricted/api/index.html' }
|
if (url?.startsWith('/auth/assets/')) return url
|
||||||
for (const [path, target] of Object.entries(routes)) {
|
// Vite serves main app routes
|
||||||
if ([`/auth/${path}`, `/auth/${path}/`, `/${path}`, `/${path}/`].includes(url)) return target
|
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 (/^\/auth\/([a-z]+\.){4}[a-z]+\/?$|^\/([a-z]+\.){4}[a-z]+\/?$/.test(url)) return '/reset/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,
|
emptyOutDir: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
index: resolve(__dirname, 'index.html'),
|
auth: resolve(__dirname, 'auth/index.html'),
|
||||||
admin: resolve(__dirname, 'admin/index.html'),
|
admin: resolve(__dirname, 'auth/admin/index.html'),
|
||||||
reset: resolve(__dirname, 'reset/index.html'),
|
restricted: resolve(__dirname, 'auth/restricted/index.html'),
|
||||||
'restricted-forward': resolve(__dirname, 'restricted/forward/index.html'),
|
host: resolve(__dirname, 'int/host/index.html'),
|
||||||
'restricted-api': resolve(__dirname, 'restricted/api/index.html'),
|
reset: resolve(__dirname, 'int/reset/index.html'),
|
||||||
host: resolve(__dirname, 'host/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]'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -439,7 +439,7 @@ class DB(DatabaseInterface):
|
|||||||
credential_model = result.scalar_one_or_none()
|
credential_model = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not credential_model:
|
if not credential_model:
|
||||||
raise ValueError("This passkey is not registered with this service")
|
raise ValueError("Credential not found")
|
||||||
return Credential(
|
return Credential(
|
||||||
uuid=UUID(bytes=credential_model.uuid),
|
uuid=UUID(bytes=credential_model.uuid),
|
||||||
credential_id=credential_model.credential_id,
|
credential_id=credential_model.credential_id,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async def general_exception_handler(_request, exc: Exception):
|
|||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def adminapp(request: Request, auth=AUTH_COOKIE):
|
async def adminapp(request: Request, auth=AUTH_COOKIE):
|
||||||
return FileResponse(frontend.file("admin/index.html"))
|
return FileResponse(frontend.file("auth", "admin", "index.html"))
|
||||||
|
|
||||||
|
|
||||||
# -------------------- Organizations --------------------
|
# -------------------- Organizations --------------------
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from fastapi import (
|
|||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
)
|
)
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.security import HTTPBearer
|
from fastapi.security import HTTPBearer
|
||||||
|
|
||||||
from passkey.util import frontend
|
from passkey.util import frontend
|
||||||
@@ -36,12 +36,6 @@ app = FastAPI()
|
|||||||
app.mount("/user", user.app)
|
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)
|
@app.exception_handler(HTTPException)
|
||||||
async def http_exception_handler(_request: Request, exc: HTTPException):
|
async def http_exception_handler(_request: Request, exc: HTTPException):
|
||||||
"""Ensure auth cookie is cleared on 401 responses (JSON responses only)."""
|
"""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)
|
return Response(status_code=204, headers=remote_headers)
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
# Let global handler clear cookie; still return HTML surface instead of JSON
|
# 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
|
status = e.status_code
|
||||||
# If 401 we still want cookie cleared; rely on handler by raising again not feasible (we need HTML)
|
# If 401 we still want cookie cleared; rely on handler by raising again not feasible (we need HTML)
|
||||||
if status == 401:
|
if status == 401:
|
||||||
|
|||||||
@@ -55,9 +55,18 @@ app.mount("/auth/admin/", admin.app)
|
|||||||
app.mount("/auth/api/", api.app)
|
app.mount("/auth/api/", api.app)
|
||||||
app.mount("/auth/ws/", ws.app)
|
app.mount("/auth/ws/", ws.app)
|
||||||
app.mount(
|
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
|
# 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
|
# / 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"))
|
cur_host = hostutil.normalize_host(request.headers.get("host"))
|
||||||
cfg_normalized = hostutil.normalize_host(cfg_host)
|
cfg_normalized = hostutil.normalize_host(cfg_host)
|
||||||
if cur_host and cfg_normalized and cur_host != cfg_normalized:
|
if cur_host and cfg_normalized and cur_host != cfg_normalized:
|
||||||
return FileResponse(frontend.file("host", "index.html"))
|
return FileResponse(frontend.file("int", "host", "index.html"))
|
||||||
return FileResponse(frontend.file("index.html"))
|
return FileResponse(frontend.file("auth", "index.html"))
|
||||||
|
|
||||||
|
|
||||||
@app.get("/admin", include_in_schema=False)
|
@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."""
|
"""Serve the reset app directly with an injected reset token."""
|
||||||
if not passphrase.is_well_formed(reset):
|
if not passphrase.is_well_formed(reset):
|
||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
return FileResponse(frontend.file("reset", "index.html"))
|
return FileResponse(frontend.file("int", "reset", "index.html"))
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ from passkey.util import useragent
|
|||||||
from .. import aaguid
|
from .. import aaguid
|
||||||
from ..authsession import session_key
|
from ..authsession import session_key
|
||||||
from ..globals import db
|
from ..globals import db
|
||||||
from ..util import hostutil, permutil
|
from . import hostutil, permutil, tokens
|
||||||
from ..util.tokens import encode_session_key
|
|
||||||
|
|
||||||
|
|
||||||
def _format_datetime(dt):
|
def _format_datetime(dt):
|
||||||
@@ -108,7 +107,7 @@ async def format_user_info(
|
|||||||
for entry in session_records:
|
for entry in session_records:
|
||||||
sessions_payload.append(
|
sessions_payload.append(
|
||||||
{
|
{
|
||||||
"id": encode_session_key(entry.key),
|
"id": tokens.encode_session_key(entry.key),
|
||||||
"host": entry.host,
|
"host": entry.host,
|
||||||
"ip": entry.ip,
|
"ip": entry.ip,
|
||||||
"user_agent": useragent.compact_user_agent(entry.user_agent),
|
"user_agent": useragent.compact_user_agent(entry.user_agent),
|
||||||
|
|||||||
Reference in New Issue
Block a user