Add frontend auth, dashboard & router guards
Introduce a complete frontend auth flow and protected dashboard. - Add auth session module (GUI/src/services/authSession.ts) with fetchCurrentUser, login, logout, caching and structured errors. - Add Dashboard page (GUI/src/routes/dashboard/Dashboard.vue) and a protected Dashboard route (meta.requiresAuth) at '/'. - Move public landing page to /welcome and mark it Visibility.Unauthenticated; update 404 and Impressum links. - Implement router guard (GUI/src/router/index.ts) to redirect unauthenticated users to Login and prevent logged-in users from accessing guest-only pages. - Update routes layout (GUI/src/plugins/routesLayout.ts) to include authenticated/unauthenticated visibility and dashboard entry. - Update Layout.vue to track current user, show username/menu, conditionally render sidebar items, add logout flow and error snackbar, and insert visual divider before auth-only items. - Convert Login.vue into a working login form with loading state, error handling and redirect after success. - Update codexInfo.md to document the new auth features and related UI/route changes.
This commit is contained in:
@@ -1,7 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { AuthRequestError, login } from '@/services/authSession'
|
||||
|
||||
const showPassword = ref(false)
|
||||
const userName = ref('')
|
||||
const password = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const submitDisabled = computed(
|
||||
() => isSubmitting.value || userName.value.trim().length === 0 || password.value.length === 0,
|
||||
)
|
||||
|
||||
const redirectPath = computed(() => {
|
||||
const redirectQuery = route.query.redirect
|
||||
if (typeof redirectQuery === 'string' && redirectQuery.startsWith('/')) {
|
||||
return redirectQuery
|
||||
}
|
||||
|
||||
return '/'
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
if (submitDisabled.value) {
|
||||
errorMessage.value = 'Bitte Benutzername und Passwort eingeben.'
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await login({
|
||||
userName: userName.value.trim(),
|
||||
password: password.value,
|
||||
})
|
||||
|
||||
await router.replace(redirectPath.value)
|
||||
} catch (error) {
|
||||
if (error instanceof AuthRequestError) {
|
||||
errorMessage.value = error.message
|
||||
} else {
|
||||
errorMessage.value = 'Anmeldung fehlgeschlagen. Bitte versuche es erneut.'
|
||||
}
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -30,22 +79,35 @@ const showPassword = ref(false)
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<v-form class="login-form hoard-panel" @submit.prevent>
|
||||
<v-form class="login-form hoard-panel" @submit.prevent="handleSubmit">
|
||||
<div class="form-head">
|
||||
<h2>Login</h2>
|
||||
<p>Melde dich mit deinem bestehenden Konto an.</p>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
border="start"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<v-text-field
|
||||
label="E-Mail"
|
||||
type="email"
|
||||
v-model="userName"
|
||||
label="Benutzername"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-email-outline"
|
||||
autocomplete="email"
|
||||
prepend-inner-icon="mdi-account-outline"
|
||||
autocomplete="username"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Passwort"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
@@ -53,17 +115,27 @@ const showPassword = ref(false)
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
|
||||
<div class="form-meta">
|
||||
<v-checkbox hide-details color="primary" density="compact" label="Angemeldet bleiben" />
|
||||
<v-btn variant="text" size="small">Passwort vergessen?</v-btn>
|
||||
<p>Anmeldung erfolgt per sicherem Session-Cookie.</p>
|
||||
</div>
|
||||
|
||||
<v-btn type="submit" color="primary" block size="large" prepend-icon="mdi-login">Anmelden</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
block
|
||||
size="large"
|
||||
prepend-icon="mdi-login"
|
||||
:loading="isSubmitting"
|
||||
:disabled="submitDisabled"
|
||||
>
|
||||
Anmelden
|
||||
</v-btn>
|
||||
|
||||
<v-btn variant="outlined" block to="/" prepend-icon="mdi-home">Zur Startseite</v-btn>
|
||||
<v-btn variant="outlined" block to="/welcome" prepend-icon="mdi-home">Zur Startseite</v-btn>
|
||||
</v-form>
|
||||
</section>
|
||||
</v-container>
|
||||
@@ -132,8 +204,13 @@ h1 {
|
||||
.form-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.form-meta p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
@@ -179,11 +256,6 @@ h1 {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
:deep(.form-meta .v-btn) {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
:deep(.login-form .v-btn) {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user