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:
+23
-3
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user