Make the login/reset/forbidden dialogs look better.

This commit is contained in:
Leo Vasanko
2025-09-30 17:03:51 -06:00
parent ed7d3ee0fc
commit 382341e5ee
5 changed files with 171 additions and 89 deletions

View File

@@ -2,17 +2,25 @@
<div class="app-shell">
<StatusMessage />
<main class="app-main">
<LoginView v-if="store.currentView === 'login'" />
<ProfileView v-if="store.currentView === 'profile'" />
<DeviceLinkView v-if="store.currentView === 'device-link'" />
<ResetView v-if="store.currentView === 'reset'" />
<PermissionDeniedView v-if="store.currentView === 'permission-denied'" />
<!-- Only render views after authentication status is determined -->
<template v-if="initialized">
<LoginView v-if="store.currentView === 'login'" />
<ProfileView v-if="store.currentView === 'profile'" />
<DeviceLinkView v-if="store.currentView === 'device-link'" />
<ResetView v-if="store.currentView === 'reset'" />
<PermissionDeniedView v-if="store.currentView === 'permission-denied'" />
</template>
<!-- Show loading state while determining auth status -->
<div v-else class="loading-container">
<div class="loading-spinner"></div>
<p>Loading...</p>
</div>
</main>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import StatusMessage from '@/components/StatusMessage.vue'
import LoginView from '@/components/LoginView.vue'
@@ -22,6 +30,7 @@ import ResetView from '@/components/ResetView.vue'
import PermissionDeniedView from '@/components/PermissionDeniedView.vue'
const store = useAuthStore()
const initialized = ref(false)
onMounted(async () => {
// Detect restricted mode:
@@ -52,10 +61,43 @@ onMounted(async () => {
}
try {
await store.loadUserInfo()
initialized.value = true
store.selectView()
} catch (error) {
console.log('Failed to load user info:', error)
store.currentView = 'login'
initialized.value = true
store.selectView()
}
store.selectView()
})
</script>
<style scoped>
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
gap: 1rem;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--color-border);
border-top: 4px solid var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-container p {
color: var(--color-text-muted);
margin: 0;
}
</style>

View File

@@ -597,3 +597,49 @@ th {
top: 1rem;
}
}
/* Dialog styles for auth views */
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog-container {
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
}
.dialog-content {
flex: none;
width: 100%;
max-width: 480px;
padding: 2rem;
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
border: 1px solid var(--color-border);
}
.dialog-content--wide {
max-width: 540px;
}
.dialog-content--narrow {
max-width: 420px;
}
@media (max-width: 720px) {
.dialog-content {
padding: 1.5rem;
}
}

View File

@@ -1,23 +1,25 @@
<template>
<section class="view-root view-login">
<div class="view-content view-content--narrow">
<header class="view-header">
<h1>🔐 {{ (authStore.settings?.rp_name || 'Passkey') + ' Login' }}</h1>
<p class="view-lede">Sign in securely with a device you trust.</p>
</header>
<section class="section-block">
<form class="section-body" @submit.prevent="handleLogin">
<button
type="submit"
class="btn-primary"
:disabled="authStore.isLoading"
>
{{ authStore.isLoading ? 'Authenticating...' : 'Login with Your Device' }}
</button>
</form>
</section>
<div class="dialog-backdrop">
<div class="dialog-container">
<div class="dialog-content dialog-content--narrow">
<header class="view-header">
<h1>🔐 {{ (authStore.settings?.rp_name || location.origin)}}</h1>
<p class="view-lede">User authentication is required for access.</p>
</header>
<section class="section-block">
<form class="section-body" @submit.prevent="handleLogin">
<button
type="submit"
class="btn-primary"
:disabled="authStore.isLoading"
>
{{ authStore.isLoading ? 'Authenticating...' : 'Login with Your Device' }}
</button>
</form>
</section>
</div>
</div>
</section>
</div>
</template>
<script setup>
@@ -44,16 +46,12 @@ const handleLogin = async () => {
</script>
<style scoped>
.view-content--narrow {
max-width: 420px;
}
.view-lede {
margin: 0;
color: var(--color-text-muted);
}
.view-login .section-body {
.section-body {
gap: 1.5rem;
}

View File

@@ -1,26 +1,28 @@
<template>
<section class="view-root view-denied">
<div class="view-content view-content--narrow">
<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 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>
<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>
</section>
</div>
</div>
</section>
</div>
</template>
<script setup>
import { useAuthStore } from '@/stores/auth'
@@ -42,10 +44,6 @@ async function logout() {
}
</script>
<style scoped>
.view-content--narrow {
max-width: 540px;
}
.view-lede {
margin: 0;
color: var(--color-text-muted);

View File

@@ -1,37 +1,39 @@
<template>
<section class="view-root view-reset">
<div class="view-content view-content--narrow">
<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'"
<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"
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>
@click="register"
>
{{ authStore.isLoading ? 'Registering…' : 'Register Passkey' }}
</button>
</div>
</section>
</div>
</div>
</section>
</div>
</template>
<script setup>
@@ -63,10 +65,6 @@ async function register() {
</script>
<style scoped>
.view-content--narrow {
max-width: 480px;
}
.view-lede {
margin: 0;
color: var(--color-text-muted);