feat(admin): archive users and their profiles permanently
All checks were successful
Build and Release FEDEO / verify (push) Successful in 1m5s
Build and Release FEDEO / build-and-release (push) Successful in 6m53s

This commit is contained in:
2026-10-15 14:22:12 +02:00
parent 5dc9e686fc
commit 4ffafef3f1
26 changed files with 519 additions and 75 deletions

View File

@@ -11,6 +11,10 @@ const userId = route.params.id as string
const loading = ref(true)
const saving = ref(false)
const resettingPassword = ref(false)
const archiving = ref(false)
const archiveModalOpen = ref(false)
const profileStore = useProfileStore()
const isOwnUser = computed(() => userId === (auth.user?.id || auth.user?.user_id))
const resetPasswordModalOpen = ref(false)
const resetPasswordInput = ref("")
const resetPasswordResult = ref("")
@@ -102,7 +106,7 @@ const fetchUser = async () => {
loading.value = true
try {
const overview = await admin.getOverview()
const overview = await admin.getOverview(true)
roles.value = overview.roles
tenants.value = overview.tenants
unassignedProfiles.value = overview.unassignedProfiles
@@ -136,7 +140,7 @@ const fetchUser = async () => {
}
const saveUser = async () => {
if (!userForm.value || saving.value) return
if (!userForm.value || userForm.value.archived_at || saving.value || archiving.value) return
saving.value = true
normalizeUserAssignments()
@@ -172,8 +176,24 @@ const saveUser = async () => {
}
}
const archiveUser = async () => {
if (!userForm.value || userForm.value.archived_at || archiving.value || isOwnUser.value) return
archiving.value = true
try {
await admin.archiveUser(userForm.value.id)
archiveModalOpen.value = false
await fetchUser()
if (auth.activeTenant) await profileStore.fetchData()
toast.add({ title: "Benutzer dauerhaft archiviert", description: "Alle zugehörigen Profile wurden archiviert und ausgeblendet.", color: "success" })
} catch (err: any) {
toast.add({ title: "Benutzer konnte nicht archiviert werden", description: err?.data?.error || err?.message || "Unbekannter Fehler", color: "error" })
} finally {
archiving.value = false
}
}
const resetPassword = async () => {
if (!userForm.value || resettingPassword.value) return
if (!userForm.value || userForm.value.archived_at || resettingPassword.value || archiving.value) return
const password = resetPasswordInput.value.trim()
if (password && password.length < 8) {
@@ -248,21 +268,40 @@ onMounted(async () => {
</UButton>
</template>
<template #right>
<UButton
v-if="userForm && !userForm.archived_at && !isOwnUser"
color="error"
variant="soft"
icon="i-heroicons-archive-box"
:disabled="saving || resettingPassword || archiving"
@click="archiveModalOpen = true"
>
Dauerhaft archivieren
</UButton>
<UButton
color="warning"
variant="soft"
icon="i-heroicons-key"
:disabled="loading || !userForm || !!userForm.archived_at || archiving || saving"
@click="openResetPasswordModal"
>
Passwort zurücksetzen
</UButton>
<UButton color="primary" :loading="saving" @click="saveUser">
<UButton color="primary" :loading="saving" :disabled="loading || !userForm || !!userForm.archived_at || archiving || resettingPassword" @click="saveUser">
Speichern
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UAlert
v-if="userForm?.archived_at"
title="Benutzer dauerhaft archiviert"
:description="`Archiviert am ${new Date(userForm.archived_at).toLocaleString('de-DE')}. Die Anmeldung ist gesperrt. Alle zugehörigen Profile sind archiviert und ausgeblendet.`"
color="warning"
variant="soft"
class="mb-4"
/>
<UCard v-if="!loading && userForm">
<div class="flex items-start justify-between gap-4 mb-6">
<div>
@@ -273,7 +312,7 @@ onMounted(async () => {
<USeparator label="Benutzer" />
<UForm :state="userForm" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
<UForm :disabled="!!userForm.archived_at || archiving" :state="userForm" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
<UFormField label="E-Mail">
<UInput v-model="userForm.email" />
</UFormField>
@@ -341,6 +380,7 @@ onMounted(async () => {
<UFormField label="Rolle">
<USelectMenu
:disabled="!!userForm.archived_at || archiving"
:model-value="getRoleForTenant(tenantId)"
:items="getRoleOptionsForTenant(tenantId)"
value-key="value"
@@ -352,6 +392,7 @@ onMounted(async () => {
<UFormField label="Freies Profil">
<USelectMenu
:disabled="!!userForm.archived_at || archiving"
:model-value="getProfileAssignmentForTenant(tenantId)"
:items="[
{ label: 'Neues Profil erzeugen', value: null },
@@ -390,7 +431,7 @@ onMounted(async () => {
variant="subtle"
color="gray"
>
{{ profile.full_name || `${profile.first_name} ${profile.last_name}` }} · Tenant {{ profile.tenant_id }}
{{ profile.full_name || `${profile.first_name} ${profile.last_name}` }} · Tenant {{ profile.tenant_id }}{{ profile.archived_at ? " · Archiviert" : "" }}
</UBadge>
</div>
</UCard>
@@ -398,6 +439,27 @@ onMounted(async () => {
<USkeleton v-if="loading" class="h-80" />
</UDashboardPanelContent>
<UModal v-model:open="archiveModalOpen" :dismissible="!archiving">
<template #content>
<UCard>
<template #header>
<div class="text-lg font-semibold">Benutzer dauerhaft archivieren</div>
</template>
<div class="space-y-3 text-sm">
<p><strong>{{ userForm?.display_name }}</strong> ({{ userForm?.email }}) wird dauerhaft archiviert.</p>
<p>Die Anmeldung wird gesperrt, bestehende Sitzungen und API-Schlüssel werden ungültig. Alle {{ userForm?.profiles.length || 0 }} zugehörigen Profile werden in sämtlichen Tenants archiviert und aus Listen und Auswahllisten ausgeblendet.</p>
<p>Bestehende Dokumente, Zeiterfassungen und Zuordnungen bleiben erhalten. Die Archivierung kann nicht rückgängig gemacht werden.</p>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<UButton variant="soft" color="neutral" :disabled="archiving" @click="archiveModalOpen = false">Abbrechen</UButton>
<UButton color="error" icon="i-heroicons-archive-box" :loading="archiving" @click="archiveUser">Dauerhaft archivieren</UButton>
</div>
</template>
</UCard>
</template>
</UModal>
<UModal v-model:open="resetPasswordModalOpen">
<template #content>
<UCard>