feat(dsgvo): Art. 17 Anonymisierung von Mitarbeiterkonten
Statt Hard-Delete (kollidiert mit Lohn-/Steuer-Aufbewahrung) werden bei
DSGVO-Löschanfrage die personenbezogenen Felder der bestehenden User-Zeile
überschrieben; referenzierte Daten (Zeit/Abwesenheit/Auszahlung/AuditLog)
bleiben entkoppelt erhalten.
- users.anonymized_at (Migration 0040); UserOut.anonymized_at.
- user_service.anonymize_user: überschreibt Name/E-Mail (eindeutig anon-<id>@deleted.local),
entfernt Auth-Material (Passwort/PIN/QR/NFC/TOTP/LDAP-DN), is_active=False, löscht Sessions,
AuditLog 'user_anonymized'. Personalnummer bleibt reserviert (kein Personenbezug mehr).
Selbst-Anonymisierung (400), Doppelanonymisierung/Reaktivierung anonymisierter Konten (409).
- POST /users/{id}/anonymize (COMPANY_ADMIN/SUPER_ADMIN, mandantengeprüft).
- UsersPage: "Anonymisieren"-Button mit Sicherheitsabfrage + "Anonymisiert"-Badge.
193/193 Tests grün. Deployed 137 + 164.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ interface UserOut {
|
||||
can_manual_time_entry: boolean
|
||||
entry_date: string | null
|
||||
exit_date: string | null
|
||||
anonymized_at: string | null
|
||||
last_login: string | null
|
||||
created_at: string
|
||||
}
|
||||
@@ -221,6 +222,21 @@ export function UsersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAnonymize(user: UserOut) {
|
||||
if (!confirm(
|
||||
`DSGVO-Anonymisierung von ${user.full_name}?\n\n` +
|
||||
`Name und E-Mail werden unwiderruflich entfernt, das Konto wird deaktiviert. ` +
|
||||
`Zeit-, Abwesenheits- und Auszahlungsdaten bleiben (gesetzliche Aufbewahrung), ` +
|
||||
`aber ohne Personenbezug. Nicht umkehrbar.`
|
||||
)) return
|
||||
try {
|
||||
await api.post(`/users/${user.id}/anonymize`, {})
|
||||
load()
|
||||
} catch (e: unknown) {
|
||||
alert(e instanceof Error ? e.message : 'Fehler')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditRole() {
|
||||
if (!editUser) return
|
||||
setEditLoading(true)
|
||||
@@ -415,14 +431,19 @@ export function UsersPage() {
|
||||
</td>
|
||||
<td className='px-4 py-3'>
|
||||
<Badge
|
||||
text={u.is_active ? 'Aktiv' : 'Inaktiv'}
|
||||
color={u.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-600'}
|
||||
text={u.anonymized_at ? 'Anonymisiert' : u.is_active ? 'Aktiv' : 'Inaktiv'}
|
||||
color={u.anonymized_at ? 'bg-gray-200 text-gray-500' : u.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-600'}
|
||||
/>
|
||||
</td>
|
||||
<td className='px-4 py-3 text-gray-500'>
|
||||
{u.last_login ? new Date(u.last_login).toLocaleDateString('de-DE') : '—'}
|
||||
</td>
|
||||
<td className='px-4 py-3'>
|
||||
{u.anonymized_at ? (
|
||||
<div className='flex justify-end text-xs text-gray-400 italic'>
|
||||
anonymisiert am {new Date(u.anonymized_at).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex gap-2 justify-end'>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -450,7 +471,15 @@ export function UsersPage() {
|
||||
Reaktivieren
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleAnonymize(u)}
|
||||
className='text-xs text-gray-400 hover:text-red-600 hover:underline'
|
||||
title='DSGVO Art. 17 – Personenbezug entfernen (nicht umkehrbar)'
|
||||
>
|
||||
Anonymisieren
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user