diff --git a/features/INDEX.md b/features/INDEX.md new file mode 100644 index 0000000..6d48924 --- /dev/null +++ b/features/INDEX.md @@ -0,0 +1,39 @@ +# Feature Index + +> Central tracking for all features. Updated by skills automatically. + +## Status Legend +- **Planned** - Requirements written, ready for development +- **In Progress** - Currently being built +- **In Review** - QA testing in progress +- **Deployed** - Live in production + +## Features + +| ID | Feature | Status | Spec | Created | +|----|---------|--------|------|---------| +| PROJ-1 | Nutzer-Authentifizierung & Rollen (User/Admin) | Deployed | [PROJ-1](PROJ-1-authentifizierung-und-rollen.md) | 2026-03-12 | +| PROJ-2 | E-Mail-Import: EML/MBOX Upload | In Progress | [PROJ-2](PROJ-2-import-eml-mbox.md) | 2026-03-12 | +| PROJ-3 | E-Mail-Import: IMAP-Verbindung | Deployed | [PROJ-3](PROJ-3-import-imap.md) | 2026-03-12 | +| PROJ-4 | E-Mail-Import: SMTP-Eingang via BCC (primär) | Deployed | [PROJ-4](PROJ-4-import-smtp.md) | 2026-03-12 | +| PROJ-5 | E-Mail-Speicherung & Volltext-Indexierung | Deployed | [PROJ-5](PROJ-5-speicherung-und-indexierung.md) | 2026-03-12 | +| PROJ-6 | Volltext-Suche & Filterung | Deployed | [PROJ-6](PROJ-6-volltext-suche.md) | 2026-03-12 | +| PROJ-7 | E-Mail-Ansicht (Lesen & Anhänge) | Deployed | [PROJ-7](PROJ-7-email-ansicht.md) | 2026-03-12 | +| PROJ-8 | Automatischer IMAP-Sync (Cron-Job) | Deployed | [PROJ-8](PROJ-8-imap-auto-sync.md) | 2026-03-12 | +| PROJ-9 | Ordner- & Label-Verwaltung | In Progress | [PROJ-9](PROJ-9-ordner-und-labels.md) | 2026-03-12 | +| PROJ-10 | Admin-Bereich: Nutzer- & Postfachverwaltung | Deployed | [PROJ-10](PROJ-10-admin-bereich.md) | 2026-03-12 | +| PROJ-11 | Audit-Log & Compliance-Berichte | Deployed | [PROJ-11](PROJ-11-audit-log.md) | 2026-03-12 | +| PROJ-12 | E-Mail-Export (EML/PDF) | Deployed | [PROJ-12](PROJ-12-export.md) | 2026-03-12 | +| PROJ-13 | REST API für externe CRM-Anbindung | In Progress | [PROJ-13](PROJ-13-rest-api-crm.md) | 2026-03-13 | +| PROJ-14 | E-Mail-Import: POP3-Verbindung | In Progress | [PROJ-14](PROJ-14-import-pop3.md) | 2026-03-13 | +| PROJ-15 | CLI Import & Export (archivmail-User) | Deployed | [PROJ-15](PROJ-15-cli-import-export.md) | 2026-03-13 | +| PROJ-16 | LDAP / Active Directory Anbindung | In Progress | [PROJ-16](PROJ-16-ldap-active-directory.md) | 2026-03-13 | + +| PROJ-17 | Admin Dashboard – Systemauslastung & Archiv-Übersicht | Deployed | [PROJ-17](PROJ-17-system-dashboard.md) | 2026-03-14 | +| PROJ-18 | E-Mail Integritätsprüfung | Deployed | [PROJ-18](PROJ-18-integritaetspruefung.md) | 2026-03-14 | +| PROJ-19 | Mailpiler → archivmail Migrationstool | Deployed | [PROJ-19](PROJ-19-import-piler.md) | 2026-03-17 | +| PROJ-20 | Nutzer-Löschung & E-Mail-Verbleib (GoBD-konform) | Deployed | [PROJ-20](PROJ-20-nutzer-loeschung.md) | 2026-03-17 | + + + +## Next Available ID: PROJ-21 diff --git a/features/PROJ-20-nutzer-loeschung.md b/features/PROJ-20-nutzer-loeschung.md new file mode 100644 index 0000000..29c660a --- /dev/null +++ b/features/PROJ-20-nutzer-loeschung.md @@ -0,0 +1,58 @@ +--- +id: PROJ-20 +title: Nutzer-Löschung & E-Mail-Verbleib (GoBD-konform) +status: Deployed +created: 2026-03-17 +--- + +## Ziel + +Definiert und implementiert das Verhalten beim Löschen eines Benutzerkontos in Bezug auf archivierte E-Mails, IMAP-Verbindungen und Compliance-Anforderungen. + +## Hintergrund + +E-Mails im Archiv sind Unternehmenseigentum und nicht an einen Benutzeraccount gebunden. Die GoBD schreibt eine Aufbewahrungspflicht von 6–10 Jahren vor, die auch nach Ausscheiden eines Mitarbeiters gilt. Daher dürfen E-Mails bei einer Konto-Löschung **nie automatisch mitgelöscht** werden. + +## Verhalten + +### E-Mails +- E-Mails haben **keinen Fremdschlüssel auf `users`** — sie sind global im Archiv gespeichert. +- Bei jeder Art von Konto-Entfernung bleiben alle E-Mails vollständig erhalten. +- Keine automatische Löschung, kein Cascade. + +### IMAP-Verbindungen +- Beim **endgültigen Löschen** eines Nutzers werden alle IMAP-Konten (`imap_accounts WHERE owner = username`) mitgelöscht. +- Sync-Jobs laufen nicht mehr ins Leere. + +### Audit-Log +- Jede Löschung wird im Audit-Log erfasst mit: `user_id`, `username`, `role`, Anzahl gelöschter IMAP-Konten und dem Hinweis `emails retained per GoBD`. + +## Zwei Aktionen im Admin-UI + +| Aktion | Effekt | Rückgängig | +|--------|--------|------------| +| **Deaktivieren** (empfohlen) | `active = false`, Login gesperrt, Konto + IMAP erhalten | Ja — jederzeit reaktivierbar | +| **Endgültig löschen** | Account + IMAP-Konten entfernt, E-Mails bleiben | Nein | + +## Implementierung + +### Backend (`internal/api/server.go`) +- `handleDeleteUser`: Lädt Nutzername vor Löschung, ruft `imapStore.DeleteByOwner()`, schreibt erweitertes Audit-Log. + +### IMAP Store (`internal/imap/store.go`) +- Neue Methode: `DeleteByOwner(ctx, username) (int, error)` — löscht alle IMAP-Konten eines Nutzers. + +### Frontend (`src/app/admin/page.tsx`) +- Löschen-Button öffnet Dialog statt `confirm()`. +- Dialog zeigt: GoBD-Hinweis, Erklärung beider Optionen, Deaktivieren-Button (primär), Löschen-Button (destruktiv). +- Deaktivieren ruft `PATCH /api/users/{id}` mit `{ active: false }`. +- Löschen ruft `DELETE /api/users/{id}`. + +## Akzeptanzkriterien + +- [x] Löschen eines Nutzers entfernt keine E-Mails +- [x] Löschen eines Nutzers entfernt dessen IMAP-Konten +- [x] Audit-Log enthält Nutzername, Rolle und IMAP-Anzahl +- [x] Admin-UI zeigt Warn-Dialog mit GoBD-Hinweis +- [x] Deaktivieren als separate, bevorzugte Aktion verfügbar +- [x] Bereits deaktivierte Nutzer: Deaktivieren-Button inaktiv diff --git a/internal/api/server.go b/internal/api/server.go index 442bc7e..d746c1b 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -385,22 +385,42 @@ func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) { return } + // Fetch user info before deletion for audit log and IMAP cleanup + target, err := s.users.GetByID(id) + if err != nil { + writeError(w, http.StatusNotFound, "user not found") + return + } + if err := s.users.DeleteSafe(id); err != nil { if err.Error() == "userstore: cannot delete last admin" { writeError(w, http.StatusConflict, "cannot delete the last active admin") return } - writeError(w, http.StatusNotFound, err.Error()) + writeError(w, http.StatusInternalServerError, err.Error()) return } + // Remove all IMAP accounts that belonged to this user + imapDeleted := 0 + if s.imapStore != nil { + if n, err := s.imapStore.DeleteByOwner(r.Context(), target.Username); err != nil { + s.logger.Warn("delete user: could not remove IMAP accounts", "user", target.Username, "err", err) + } else { + imapDeleted = n + } + } + sess := sessionFromCtx(r.Context()) s.audlog.Log(audit.Entry{ EventType: audit.EventUserMgmt, Username: sess.Username, IPAddress: remoteIP(r), - Detail: fmt.Sprintf("deleted user %d", id), - Success: true, + Detail: fmt.Sprintf( + "deleted user %d (%s, role=%s); %d IMAP account(s) removed; emails retained per GoBD", + id, target.Username, target.Role, imapDeleted, + ), + Success: true, }) writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) diff --git a/internal/imap/store.go b/internal/imap/store.go index a43ba59..1b576cb 100644 --- a/internal/imap/store.go +++ b/internal/imap/store.go @@ -238,6 +238,16 @@ func (s *Store) Delete(ctx context.Context, id int64) error { return nil } +// DeleteByOwner removes all IMAP accounts belonging to the given username. +// Returns the number of accounts deleted. +func (s *Store) DeleteByOwner(ctx context.Context, username string) (int, error) { + tag, err := s.pool.Exec(ctx, `DELETE FROM imap_accounts WHERE owner = $1`, username) + if err != nil { + return 0, fmt.Errorf("imap store: delete by owner: %w", err) + } + return int(tag.RowsAffected()), nil +} + // UpdateExcluded sets the list of excluded folders for an account. func (s *Store) UpdateExcluded(ctx context.Context, id int64, excluded []string) error { _, err := s.pool.Exec(ctx, `UPDATE imap_accounts SET excluded_folders = $1 WHERE id = $2`, excluded, id) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 3a27510..7222056 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -108,6 +108,11 @@ export default function AdminPage() { const [resetPasswordError, setResetPasswordError] = useState(""); const [resetPasswordLoading, setResetPasswordLoading] = useState(false); + // Delete confirmation dialog + const [deleteDialogUser, setDeleteDialogUser] = useState(null); + const [deleteActionLoading, setDeleteActionLoading] = useState<"deactivate" | "delete" | null>(null); + const [deleteDialogError, setDeleteDialogError] = useState(""); + // Audit state const [auditEntries, setAuditEntries] = useState([]); 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 @@ -1203,6 +1225,61 @@ export default function AdminPage() { + + {/* Nutzer-Löschung Dialog */} + { if (!open) setDeleteDialogUser(null); }}> + + + Benutzer entfernen + + Was soll mit dem Konto {deleteDialogUser?.username} passieren? + + + +
+ Hinweis (GoBD): E-Mails bleiben unabhängig von dieser Aktion im Archiv erhalten. Die gesetzliche Aufbewahrungspflicht besteht auch nach Ausscheiden des Mitarbeiters. +
+ +
+
+

Konto deaktivieren (empfohlen)

+

+ Login wird gesperrt. Konto und IMAP-Verbindungen bleiben erhalten und können reaktiviert werden. +

+
+
+

Konto endgültig löschen

+

+ Account und alle IMAP-Verbindungen werden dauerhaft entfernt. Nicht rückgängig zu machen. +

+
+
+ + {deleteDialogError && ( +

{deleteDialogError}

+ )} + + + + + + +
+
); }