Refactor user-profile, restricted access and reset token registration as separate apps so the frontend does not need to guess which context it is running in.

Support user-navigable URLs at / as well as /auth/, allowing for a dedicated authentication site with pretty URLs.
This commit is contained in:
2025-10-03 03:42:01 +00:00
parent b4871c671f
commit 2f1578c4bc
23 changed files with 668 additions and 295 deletions
+1 -7
View File
@@ -32,13 +32,7 @@ const handleLogin = async () => {
authStore.showMessage('Starting authentication...', 'info')
await authStore.authenticate()
authStore.showMessage('Authentication successful!', 'success', 2000)
if (authStore.restrictedMode) {
location.reload()
} else if (location.pathname === '/auth/') {
authStore.currentView = 'profile'
} else {
location.reload()
}
authStore.currentView = 'profile'
} catch (error) {
authStore.showMessage(error.message, 'error')
}
@@ -1,94 +0,0 @@
<template>
<div class="dialog-backdrop">
<div class="dialog-container">
<div class="dialog-content dialog-content--wide">
<header class="view-header">
<h1>🚫 Forbidden</h1>
</header>
<section class="section-block">
<div class="section-body">
<div v-if="authStore.userInfo?.authenticated" class="user-header">
<span class="user-emoji" aria-hidden="true">{{ userEmoji }}</span>
<span class="user-name">{{ displayName }}</span>
</div>
<p>You lack the permissions required for this page.</p>
<div class="button-row">
<button class="btn-secondary" @click="back">Back</button>
<button class="btn-primary" @click="goAuth">Account</button>
<button class="btn-danger" @click="logout">Logout</button>
</div>
<p class="hint">If you believe this is an error, contact your administrator.</p>
</div>
</section>
</div>
</div>
</div>
</template>
<script setup>
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const userEmoji = '👤' // Placeholder / could be extended later if backend provides one
const displayName = authStore.userInfo?.user?.user_name || 'User'
function goAuth() {
location.href = '/auth/'
}
function back() {
if (history.length > 1) history.back()
else authStore.currentView = 'login'
}
async function logout() {
await authStore.logout()
}
</script>
<style scoped>
.view-lede {
margin: 0;
color: var(--color-text-muted);
}
.user-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.1rem;
}
.user-emoji {
font-size: 1.5rem;
line-height: 1;
}
.user-name {
font-weight: 600;
color: var(--color-heading);
}
.button-row {
width: 100%;
justify-content: stretch;
}
.button-row button {
flex: 1 1 0;
}
.hint {
font-size: 0.9rem;
color: var(--color-text-muted);
margin: 0;
}
@media (max-width: 720px) {
.button-row {
flex-direction: column;
}
.button-row button {
width: 100%;
flex: 1 1 auto;
}
}
</style>
+7 -1
View File
@@ -3,7 +3,7 @@
<div class="view-content">
<header class="view-header">
<h1>👋 Welcome!</h1>
<Breadcrumbs :entries="[{ label: 'Auth', href: '/auth/' }, ...(isAdmin ? [{ label: 'Admin', href: '/auth/admin/' }] : [])]" />
<Breadcrumbs :entries="breadcrumbEntries" />
<p class="view-lede">Manage your account details and passkeys.</p>
</header>
@@ -144,6 +144,12 @@ const openNameDialog = () => {
const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin))
const breadcrumbEntries = computed(() => {
const entries = [{ label: 'Auth', href: authStore.uiHref() }]
if (isAdmin.value) entries.push({ label: 'Admin', href: authStore.adminHomeHref() })
return entries
})
const saveName = async () => {
const name = newName.value.trim()
if (!name) {
-94
View File
@@ -1,94 +0,0 @@
<template>
<div class="dialog-backdrop">
<div class="dialog-container">
<div class="dialog-content">
<header class="view-header">
<h1>🔑 Add New Credential</h1>
<p class="view-lede">
Finish setting up your passkey to complete {{ authStore.userInfo?.session_type }}.
</p>
</header>
<section class="section-block">
<div class="section-body">
<label class="name-edit">
<span>👤 Name</span>
<input
type="text"
v-model="user_name"
:placeholder="authStore.userInfo?.user?.user_name || 'Your name'"
:disabled="authStore.isLoading"
maxlength="64"
@keyup.enter="register"
/>
</label>
<p>Proceed to complete {{ authStore.userInfo?.session_type }}:</p>
<button
class="btn-primary"
:disabled="authStore.isLoading"
@click="register"
>
{{ authStore.isLoading ? 'Registering…' : 'Register Passkey' }}
</button>
</div>
</section>
</div>
</div>
</div>
</template>
<script setup>
import { useAuthStore } from '@/stores/auth'
import passkey from '@/utils/passkey'
import { ref } from 'vue'
const authStore = useAuthStore()
const user_name = ref('')
async function register() {
authStore.isLoading = true
authStore.showMessage('Starting registration...', 'info')
try {
const result = await passkey.register(authStore.resetToken, user_name.value)
console.log('Result', result)
await authStore.setSessionCookie(result.session_token)
authStore.resetToken = null
authStore.showMessage('Passkey registered successfully!', 'success', 2000)
await authStore.loadUserInfo()
authStore.selectView()
} catch (error) {
authStore.showMessage(`Registration failed: ${error.message}`, 'error')
} finally {
authStore.isLoading = false
}
}
</script>
<style scoped>
.view-lede {
margin: 0;
color: var(--color-text-muted);
}
.name-edit {
display: flex;
flex-direction: column;
gap: 0.45rem;
font-weight: 600;
}
.name-edit span {
color: var(--color-text-muted);
font-size: 0.9rem;
}
.section-body {
gap: 1.5rem;
}
@media (max-width: 720px) {
button {
width: 100%;
}
}
</style>