Files
Hoard/GUI/src/routes/authentication/ChangePassword.vue
T
Jonas 10bf4b94ad UI refresh: animation, cards, responsive tweaks
Large frontend modernization: add route fade transition and hoard-soft-enter keyframes with prefers-reduced-motion support; introduce smoother motion tokens and stronger shadow tokens. Update global CSS to use subtle surface gradients, unified transitions, hover lift effects, focus rings and improved button/card/table/overlay styles. Wrap <router-view> in a transition and adjust brand/logo sizing and interactions. Revamp several pages/components (Home, Login, Impressum, 404, Forbidden) with adjusted typography, animated entry for sections and improved card hover states. Admin/Dashboard pages enhanced: AdminUsers gains stats, mobile card list & computed counts; AdminUserDetail and Dashboard show compact summary cards and updated styles. Documentation updated (style.md, codexInfo.md) to reflect the new modernisation rules. No API or backend changes.
2026-04-23 22:07:07 +02:00

215 lines
5.6 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { AuthRequestError, changePassword } from '@/services/authSession'
import { useAppBannersStore } from '@/stores/appBanners'
const router = useRouter()
const appBannersStore = useAppBannersStore()
const oldPassword = ref('')
const newPassword = ref('')
const newPasswordConfirm = ref('')
const showOldPassword = ref(false)
const showNewPassword = ref(false)
const showNewPasswordConfirm = ref(false)
const isSubmitting = ref(false)
const errorMessage = ref('')
const submitDisabled = computed(() => {
return (
isSubmitting.value ||
oldPassword.value.length === 0 ||
newPassword.value.length === 0 ||
newPasswordConfirm.value.length === 0
)
})
async function handleSubmit() {
if (submitDisabled.value) {
errorMessage.value = 'Bitte alle Passwortfelder ausfüllen.'
return
}
if (newPassword.value !== newPasswordConfirm.value) {
errorMessage.value = 'Die neuen Passwörter stimmen nicht überein.'
return
}
isSubmitting.value = true
errorMessage.value = ''
try {
await changePassword({
oldPassword: oldPassword.value,
newPassword: newPassword.value,
newPasswordConfirm: newPasswordConfirm.value,
})
await router.replace({
name: 'Login',
query: { passwordChanged: '1' },
})
} catch (error) {
if (error instanceof AuthRequestError) {
errorMessage.value = error.message
if (error.status === 401 || error.status === 403) {
appBannersStore.pushError('Session abgelaufen. Bitte erneut anmelden.', 'Passwort ändern')
await router.replace({ name: 'Login' })
return
}
} else {
errorMessage.value = 'Passwortänderung fehlgeschlagen. Bitte versuche es erneut.'
}
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<v-container fluid class="change-password-page hoard-page hoard-page--centered">
<section class="change-password-shell hoard-panel hoard-shell-grid">
<header class="change-password-head">
<p class="hoard-kicker">Sicherheitsvorgabe</p>
<h1>Passwort ändern</h1>
<p>Hier kannst du ganz bequem dein Passwort aktualisieren.</p>
</header>
<v-alert
v-if="errorMessage"
type="error"
variant="tonal"
border="start"
>
{{ errorMessage }}
</v-alert>
<v-form class="change-password-form" @submit.prevent="handleSubmit">
<v-text-field
v-model="oldPassword"
label="Altes Passwort"
:type="showOldPassword ? 'text' : 'password'"
variant="outlined"
prepend-inner-icon="mdi-lock-outline"
:append-inner-icon="showOldPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
autocomplete="current-password"
required
:disabled="isSubmitting"
@click:append-inner="showOldPassword = !showOldPassword"
/>
<v-divider class="change-password-divider" />
<p class="change-password-section-label">Neues Passwort</p>
<v-text-field
v-model="newPassword"
label="Neues Passwort"
:type="showNewPassword ? 'text' : 'password'"
variant="outlined"
prepend-inner-icon="mdi-lock-reset"
:append-inner-icon="showNewPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
autocomplete="new-password"
required
:disabled="isSubmitting"
@click:append-inner="showNewPassword = !showNewPassword"
/>
<v-text-field
v-model="newPasswordConfirm"
label="Neues Passwort bestätigen"
:type="showNewPasswordConfirm ? 'text' : 'password'"
variant="outlined"
prepend-inner-icon="mdi-lock-check-outline"
:append-inner-icon="showNewPasswordConfirm ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
autocomplete="new-password"
required
:disabled="isSubmitting"
@click:append-inner="showNewPasswordConfirm = !showNewPasswordConfirm"
/>
<p class="change-password-hint">
Nach erfolgreicher Änderung wirst du automatisch abgemeldet und meldest dich mit dem neuen Passwort wieder an.
</p>
<v-btn
type="submit"
color="primary"
size="large"
prepend-icon="mdi-content-save-outline"
:loading="isSubmitting"
:disabled="submitDisabled"
block
>
Passwort speichern
</v-btn>
</v-form>
</section>
</v-container>
</template>
<style scoped>
.change-password-page {
--hoard-shell-width: min(720px, 100%);
}
.change-password-shell {
gap: var(--space-4);
}
.change-password-head h1,
.change-password-head p {
margin: 0;
}
.change-password-head h1 {
margin-top: var(--space-2);
margin-bottom: var(--space-2);
}
.change-password-head p {
color: var(--color-text-secondary);
}
.change-password-form {
display: grid;
gap: var(--space-3);
}
.change-password-divider {
margin-top: var(--space-1);
margin-bottom: 0;
border-color: color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%);
}
.change-password-section-label {
margin: 0;
color: var(--color-primary-700);
font-size: var(--font-size-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.change-password-hint {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
@media (prefers-reduced-motion: no-preference) {
.change-password-shell {
animation: hoard-soft-enter 260ms both;
}
}
@media (width <= 600px) {
.change-password-form {
gap: var(--space-2);
}
}
</style>