6c2a149f96
Introduce a global app banners store (GUI/src/stores/appBanners.ts) and switch authentication error/notification flows to use it. GUI/src/Layout.vue now consumes the banner store, replaces the single snackbar with a stacked, dismissible banner UI (styles, transitions and z-index included), and adds navigateToBrandTarget() to route the brand button based on auth state. GUI/src/routes/authentication/Login.vue was updated to push errors to the new banner store and remove the local alert. Minor route adjustments in GUI/src/plugins/routesLayout.ts: rename 'Dash' to 'Dashboard' and change Login visibility to Unauthenticated. codexInfo.md updated to document these UI/behavior changes.
257 lines
5.9 KiB
Vue
257 lines
5.9 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { AuthRequestError, login } from '@/services/authSession'
|
|
import { useAppBannersStore } from '@/stores/appBanners'
|
|
|
|
const showPassword = ref(false)
|
|
const userName = ref('')
|
|
const password = ref('')
|
|
const isSubmitting = ref(false)
|
|
const appBannersStore = useAppBannersStore()
|
|
|
|
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) {
|
|
appBannersStore.pushError('Bitte Benutzername und Passwort eingeben.', 'Anmeldung fehlgeschlagen')
|
|
return
|
|
}
|
|
|
|
isSubmitting.value = true
|
|
|
|
try {
|
|
await login({
|
|
userName: userName.value.trim(),
|
|
password: password.value,
|
|
})
|
|
|
|
await router.replace(redirectPath.value)
|
|
} catch (error) {
|
|
if (error instanceof AuthRequestError) {
|
|
appBannersStore.pushError(error.message, 'Anmeldung fehlgeschlagen')
|
|
} else {
|
|
appBannersStore.pushError(
|
|
'Anmeldung fehlgeschlagen. Bitte versuche es erneut.',
|
|
'Anmeldung fehlgeschlagen',
|
|
)
|
|
}
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<v-container fluid class="login-page hoard-page hoard-page--centered">
|
|
<section class="login-shell hoard-panel hoard-shell-grid hoard-panel-gradient">
|
|
<aside class="login-brand">
|
|
<p class="login-kicker hoard-kicker hoard-kicker--wide">Willkommen bei Hoard</p>
|
|
<h1>Anmelden und weiterarbeiten</h1>
|
|
<p class="login-intro">
|
|
Deine Dateiablage bleibt aufgeräumt, schnell und direkt im Browser bedienbar.
|
|
</p>
|
|
|
|
<ul class="login-points">
|
|
<li>
|
|
<v-icon icon="mdi-folder-outline" size="18" />
|
|
Ordner und Dateien zentral verwalten
|
|
</li>
|
|
<li>
|
|
<v-icon icon="mdi-file-document-edit-outline" size="18" />
|
|
Markdown-Dateien sofort bearbeiten
|
|
</li>
|
|
<li>
|
|
<v-icon icon="mdi-image-outline" size="18" />
|
|
Bilder und PDFs direkt als Vorschau ansehen
|
|
</li>
|
|
</ul>
|
|
</aside>
|
|
|
|
<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-text-field
|
|
v-model="userName"
|
|
label="Benutzername"
|
|
type="text"
|
|
variant="outlined"
|
|
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"
|
|
prepend-inner-icon="mdi-lock-outline"
|
|
: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">
|
|
<p>Anmeldung erfolgt per sicherem Session-Cookie.</p>
|
|
</div>
|
|
|
|
<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="/welcome" prepend-icon="mdi-home">Zur Startseite</v-btn>
|
|
</v-form>
|
|
</section>
|
|
</v-container>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.login-shell {
|
|
--hoard-shell-width: 1040px;
|
|
--hoard-gradient-angle: 115deg;
|
|
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 45%, var(--color-surface) 55%);
|
|
--hoard-gradient-end: var(--color-surface);
|
|
--hoard-gradient-end-stop: 52%;
|
|
|
|
grid-template-columns: minmax(280px, 1fr) minmax(320px, 430px);
|
|
}
|
|
|
|
h1 {
|
|
margin-bottom: var(--space-3);
|
|
max-width: 18ch;
|
|
font-size: clamp(1.9rem, 2vw + 1rem, 2.6rem);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.login-intro {
|
|
margin-bottom: var(--space-5);
|
|
max-width: 44ch;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.login-points {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
padding: 0;
|
|
margin: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.login-points li {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
color: var(--color-text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.login-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-4);
|
|
padding: var(--space-6);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.form-head h2 {
|
|
margin-bottom: var(--space-1);
|
|
font-size: 1.45rem;
|
|
}
|
|
|
|
.form-head p {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: var(--font-size-md);
|
|
}
|
|
|
|
.form-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.form-meta p {
|
|
margin: 0;
|
|
color: var(--color-text-muted);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
@media (width <= 960px) {
|
|
.login-shell {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.login-form {
|
|
padding: var(--space-5);
|
|
}
|
|
|
|
.form-meta {
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
|
|
@media (width <= 600px) {
|
|
h1 {
|
|
max-width: none;
|
|
font-size: clamp(1.55rem, 7vw, 1.95rem);
|
|
}
|
|
|
|
.login-intro {
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.login-points {
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.login-points li {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.login-form {
|
|
gap: var(--space-3);
|
|
padding: var(--space-4);
|
|
}
|
|
|
|
.form-meta {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
:deep(.login-form .v-btn) {
|
|
min-height: 44px;
|
|
}
|
|
}
|
|
</style>
|