Drafting admin app (frontend)

This commit is contained in:
Leo Vasanko 2025-08-12 13:24:27 -07:00
parent 02ac4adc77
commit e0717f005a
5 changed files with 114 additions and 1 deletions

13
frontend/admin/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/src/assets/icon.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passkey Admin</title>
</head>
<body>
<div id="admin-app"></div>
<script type="module" src="/src/admin/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,70 @@
<script setup>
import { ref, onMounted } from 'vue'
const info = ref(null)
const loading = ref(true)
const error = ref(null)
async function load() {
loading.value = true
error.value = null
try {
const res = await fetch('/auth/user-info', { method: 'POST' })
const data = await res.json()
if (data.detail) throw new Error(data.detail)
info.value = data
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<div class="container">
<h1>Passkey Admin</h1>
<p class="subtitle">Manage organizations, roles, and permissions</p>
<div v-if="loading">Loading</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else>
<div v-if="!info?.authenticated">
<p>You must be authenticated.</p>
</div>
<div v-else-if="!(info?.is_global_admin || info?.is_org_admin)">
<p>Insufficient permissions.</p>
</div>
<div v-else>
<div class="card">
<h2>User</h2>
<div>{{ info.user.user_name }} ({{ info.user.user_uuid }})</div>
<div>Role: {{ info.role?.display_name }}</div>
</div>
<div class="card">
<h2>Organization</h2>
<div>{{ info.org?.display_name }} ({{ info.org?.uuid }})</div>
<div>Role permissions: {{ info.role?.permissions?.join(', ') }}</div>
<div>Org grantable: {{ info.org?.permissions?.join(', ') }}</div>
</div>
<div class="card">
<h2>Permissions</h2>
<div>Effective: {{ info.permissions?.join(', ') }}</div>
<div>Global admin: {{ info.is_global_admin ? 'yes' : 'no' }}</div>
<div>Org admin: {{ info.is_org_admin ? 'yes' : 'no' }}</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.container { max-width: 960px; margin: 2rem auto; padding: 0 1rem; }
.subtitle { color: #888 }
.card { margin: 1rem 0; padding: 1rem; border: 1px solid #eee; border-radius: 8px; }
.error { color: #a00 }
</style>

View File

@ -0,0 +1,6 @@
import '../assets/style.css'
import { createApp } from 'vue'
import AdminApp from './AdminApp.vue'
createApp(AdminApp).mount('#admin-app')

View File

@ -1,6 +1,7 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { resolve } from 'node:path'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/ // https://vite.dev/config/
@ -13,6 +14,7 @@ export default defineConfig(({ command, mode }) => ({
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
}, },
}, },
// Use absolute paths at dev, deploy under /auth/
base: command === 'build' ? '/auth/' : '/', base: command === 'build' ? '/auth/' : '/',
server: { server: {
port: 4403, port: 4403,
@ -27,6 +29,15 @@ export default defineConfig(({ command, mode }) => ({
build: { build: {
outDir: '../passkey/frontend-build', outDir: '../passkey/frontend-build',
emptyOutDir: true, emptyOutDir: true,
assetsDir: 'assets' assetsDir: 'assets',
rollupOptions: {
input: {
index: resolve(__dirname, 'index.html'),
admin: resolve(__dirname, 'admin/index.html')
},
output: {
// Ensure HTML files land as /auth/index.html and /auth/admin.html -> we will serve /auth/admin mapping in backend
}
}
} }
})) }))

View File

@ -70,6 +70,19 @@ async def redirect_to_index():
return FileResponse(STATIC_DIR / "index.html") return FileResponse(STATIC_DIR / "index.html")
@app.get("/auth/admin")
async def serve_admin():
"""Serve the admin app entry point."""
# Vite MPA builds admin as admin.html in the same outDir
admin_html = STATIC_DIR / "admin.html"
# If configured to emit admin/index.html, support that too
if not admin_html.exists():
alt = STATIC_DIR / "admin" / "index.html"
if alt.exists():
return FileResponse(alt)
return FileResponse(admin_html)
# Register API routes # Register API routes
register_api_routes(app) register_api_routes(app)
register_reset_routes(app) register_reset_routes(app)