feat(PROJ-34): Retention-Tab + pro-Mandant Aufbewahrungsfristen

- tenantstore: retention_days Spalte, GetRetentionDays/SetRetentionDays
- storage.Save(): per-tenant retention überschreibt globale config
- API: GET /api/admin/retention, PUT /api/admin/tenant/{id}/retention
- Frontend: RetentionTab mit globaler Policy-Anzeige, Mandanten-Tabelle,
  Bearbeiten-Dialog und Purge-Button (superadmin only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-31 10:37:15 +02:00
parent 5f0c7a7e6d
commit 5bbf6d0ff3
8 changed files with 399 additions and 16 deletions
+50
View File
@@ -1,7 +1,9 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
)
// handlePurge deletes all mails whose retention period has expired.
@@ -16,3 +18,51 @@ func (s *Server) handlePurge(w http.ResponseWriter, r *http.Request) {
"deleted": deleted,
})
}
// handleGetRetention returns the global retention config and per-tenant overrides.
// GET /api/admin/retention — superadmin only.
func (s *Server) handleGetRetention(w http.ResponseWriter, r *http.Request) {
tenants, err := s.tenantStore.List(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"global_retention_days": s.globalRetentionDays,
"tenants": tenants,
})
}
// handleSetTenantRetention sets retention_days for a specific tenant.
// PUT /api/admin/tenant/{id}/retention — superadmin only.
func (s *Server) handleSetTenantRetention(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
tenantID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
var body struct {
RetentionDays int `json:"retention_days"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid body")
return
}
if err := s.tenantStore.SetRetentionDays(r.Context(), tenantID, body.RetentionDays); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
sess := getSession(r)
if s.audlog != nil {
_ = s.audlog.Log(r.Context(), sess.UserID, "tenant_retention_changed", map[string]interface{}{
"tenant_id": tenantID,
"retention_days": body.RetentionDays,
})
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
+11 -3
View File
@@ -78,8 +78,9 @@ type Server struct {
tenantStore *tenantstore.Store
tenantLdapStore *ldapcfg.TenantStore
idxMgr *index.TenantIndexManager
appVersion string
moduleVersions map[string]string
appVersion string
moduleVersions map[string]string
globalRetentionDays int // from storage config (PROJ-34)
}
// SetSMTPDaemon wires the SMTP daemon into the API server after construction.
@@ -111,6 +112,11 @@ func (s *Server) SetVersion(appVersion string, modules map[string]string) {
s.moduleVersions = modules
}
// SetGlobalRetentionDays wires the global retention_days from storage config into the API server.
func (s *Server) SetGlobalRetentionDays(days int) {
s.globalRetentionDays = days
}
// New creates and wires up a new API server.
func New(
cfg config.APIConfig,
@@ -170,8 +176,10 @@ func (s *Server) routes() {
// SEC-17: Security fix actions require superadmin, not just domain_admin.
s.mux.HandleFunc("POST /api/admin/security/fix", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleSecurityFix)))
// PROJ-34: Retention purge — superadmin only
// PROJ-34: Retention — superadmin only
s.mux.HandleFunc("POST /api/admin/purge", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handlePurge)))
s.mux.HandleFunc("GET /api/admin/retention", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleGetRetention)))
s.mux.HandleFunc("PUT /api/admin/tenant/{id}/retention", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleSetTenantRetention)))
// PROJ-33: IMAP mode settings — domain_admin only
s.mux.HandleFunc("GET /api/admin/settings/imap-mode", s.authAdmin(s.handleGetIMAPMode))