feat(PROJ-20): GoBD-konforme Nutzer-Löschung mit IMAP-Cleanup und Warn-Dialog
- imap/store.go: DeleteByOwner() – löscht alle IMAP-Konten eines Nutzers - api/server.go: handleDeleteUser lädt Nutzername vor Löschung, ruft DeleteByOwner, schreibt erweitertes Audit-Log (username, role, IMAP-Count, GoBD-Hinweis) - admin/page.tsx: confirm() ersetzt durch Dialog mit GoBD-Hinweis, Deaktivieren-Option (empfohlen) und endgültigem Löschen (destruktiv) - features/PROJ-20-nutzer-loeschung.md: Feature-Spec angelegt - features/INDEX.md: PROJ-20 eingetragen, Next ID → PROJ-21 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+84
-7
@@ -108,6 +108,11 @@ export default function AdminPage() {
|
||||
const [resetPasswordError, setResetPasswordError] = useState("");
|
||||
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
|
||||
|
||||
// Delete confirmation dialog
|
||||
const [deleteDialogUser, setDeleteDialogUser] = useState<User | null>(null);
|
||||
const [deleteActionLoading, setDeleteActionLoading] = useState<"deactivate" | "delete" | null>(null);
|
||||
const [deleteDialogError, setDeleteDialogError] = useState("");
|
||||
|
||||
// Audit state
|
||||
const [auditEntries, setAuditEntries] = useState<AuditEntry[]>([]);
|
||||
const [auditTotal, setAuditTotal] = useState(0);
|
||||
@@ -290,16 +295,33 @@ export default function AdminPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteUser(u: User) {
|
||||
if (!confirm(`Benutzer "${u.username}" wirklich löschen?`)) return;
|
||||
setUserActionLoading(u.id);
|
||||
async function handleDeactivateConfirmed() {
|
||||
if (!deleteDialogUser) return;
|
||||
setDeleteActionLoading("deactivate");
|
||||
setDeleteDialogError("");
|
||||
try {
|
||||
await deleteUser(u.id);
|
||||
await updateUser(deleteDialogUser.id, { active: false });
|
||||
setDeleteDialogUser(null);
|
||||
loadUsers();
|
||||
} catch {
|
||||
setDeleteDialogError("Deaktivierung fehlgeschlagen.");
|
||||
} finally {
|
||||
setDeleteActionLoading(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteConfirmed() {
|
||||
if (!deleteDialogUser) return;
|
||||
setDeleteActionLoading("delete");
|
||||
setDeleteDialogError("");
|
||||
try {
|
||||
await deleteUser(deleteDialogUser.id);
|
||||
setDeleteDialogUser(null);
|
||||
loadUsers();
|
||||
} catch (err: unknown) {
|
||||
alert(err instanceof Error ? err.message : "Löschen fehlgeschlagen.");
|
||||
setDeleteDialogError(err instanceof Error ? err.message : "Löschen fehlgeschlagen.");
|
||||
} finally {
|
||||
setUserActionLoading(null);
|
||||
setDeleteActionLoading(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +996,7 @@ export default function AdminPage() {
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={userActionLoading === u.id}
|
||||
onClick={() => handleDeleteUser(u)}
|
||||
onClick={() => { setDeleteDialogUser(u); setDeleteDialogError(""); }}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
@@ -1203,6 +1225,61 @@ export default function AdminPage() {
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Nutzer-Löschung Dialog */}
|
||||
<Dialog open={deleteDialogUser !== null} onOpenChange={(open) => { if (!open) setDeleteDialogUser(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Benutzer entfernen</DialogTitle>
|
||||
<DialogDescription>
|
||||
Was soll mit dem Konto <strong>{deleteDialogUser?.username}</strong> passieren?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="rounded-md border border-yellow-300 bg-yellow-50 p-3 text-sm text-yellow-800">
|
||||
<strong>Hinweis (GoBD):</strong> E-Mails bleiben unabhängig von dieser Aktion im Archiv erhalten. Die gesetzliche Aufbewahrungspflicht besteht auch nach Ausscheiden des Mitarbeiters.
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-md border p-3">
|
||||
<p className="font-medium text-sm">Konto deaktivieren (empfohlen)</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Login wird gesperrt. Konto und IMAP-Verbindungen bleiben erhalten und können reaktiviert werden.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-destructive/30 p-3">
|
||||
<p className="font-medium text-sm text-destructive">Konto endgültig löschen</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Account und alle IMAP-Verbindungen werden dauerhaft entfernt. Nicht rückgängig zu machen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{deleteDialogError && (
|
||||
<p className="text-sm text-destructive">{deleteDialogError}</p>
|
||||
)}
|
||||
|
||||
<DialogFooter className="flex-col gap-2 sm:flex-row">
|
||||
<Button variant="outline" onClick={() => setDeleteDialogUser(null)} disabled={deleteActionLoading !== null}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDeactivateConfirmed}
|
||||
disabled={deleteActionLoading !== null || deleteDialogUser?.active === false}
|
||||
>
|
||||
{deleteActionLoading === "deactivate" ? "Wird deaktiviert..." : "Deaktivieren"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteConfirmed}
|
||||
disabled={deleteActionLoading !== null}
|
||||
>
|
||||
{deleteActionLoading === "delete" ? "Wird gelöscht..." : "Endgültig löschen"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user