Drafting admin app (frontend)
This commit is contained in:
parent
02ac4adc77
commit
e0717f005a
13
frontend/admin/index.html
Normal file
13
frontend/admin/index.html
Normal 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>
|
70
frontend/src/admin/AdminApp.vue
Normal file
70
frontend/src/admin/AdminApp.vue
Normal 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>
|
6
frontend/src/admin/main.js
Normal file
6
frontend/src/admin/main.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import '../assets/style.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import AdminApp from './AdminApp.vue'
|
||||||
|
|
||||||
|
createApp(AdminApp).mount('#admin-app')
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user