Add admin user management and password-change flow
Introduce full admin user listing/detail endpoints and a forced password-change flow. Backend: make CurrentUserResponse.UserName nullable and add ToCurrentUserResponseAsync extension; AppUserController now exposes GET /auth/user (list) and GET /auth/user/{id} (detail) using UserManager and Admin-only policy; AuthController uses the new mapper and after successful password change clears MustChangePassword, updates UpdatedAt and persists changes (with error handling) before updating security stamp. Frontend: add admin users pages (list + detail), ChangePassword page and route, adminUsers and enhanced authSession services (typed responses, changePassword API, error mapping), router guard to redirect users with mustChangePassword=true to the change-password flow, and show success banner on login after password change. UI tweaks: separate admin section in sidebar, add password-change entries in account menu, footer sizing fixes, and various layout/UX improvements. These changes enable admin account management and enforce secure password updates across the app.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
<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 Password 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%, white 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 (width <= 600px) {
|
||||
.change-password-form {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user