feat(PROJ-51): Aufbewahrungsfristen nach Dokumentenart (Retention-Kategorien)
Fuehrt archiving_rules ein (PROJ-43-Basis: Tabelle + CRUD-API + Admin-UI) und erweitert die Retention-Logik (PROJ-34) um Regel-basierte Fristen, eine globale Mindestfrist (min_retention_days) sowie Nachvollziehbarkeit der Frist-Quelle (retain_until_source) in API und Mail-Detailansicht.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"archivmail/internal/audit"
|
||||
"archivmail/internal/storage"
|
||||
)
|
||||
|
||||
// PROJ-51: Archiving rules CRUD (minimal PROJ-43). Rules optionally carry a
|
||||
// retention_days value that determines a matching mail's retention period.
|
||||
// Superadmin only — global, cross-tenant configuration.
|
||||
|
||||
type archivingRuleBody struct {
|
||||
TenantID *int64 `json:"tenant_id"`
|
||||
CondType string `json:"condition_type"`
|
||||
Pattern string `json:"pattern"`
|
||||
Priority int `json:"priority"`
|
||||
RetentionDays *int `json:"retention_days"`
|
||||
}
|
||||
|
||||
// handleListArchivingRules returns all archiving rules.
|
||||
// GET /api/admin/archiving-rules — superadmin only.
|
||||
func (s *Server) handleListArchivingRules(w http.ResponseWriter, r *http.Request) {
|
||||
rules, err := s.store.ListArchivingRules(r.Context(), nil)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rules == nil {
|
||||
rules = []storage.ArchivingRule{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"rules": rules,
|
||||
"min_retention_days": s.store.MinRetentionDays(),
|
||||
})
|
||||
}
|
||||
|
||||
// handleCreateArchivingRule creates a new archiving rule.
|
||||
// POST /api/admin/archiving-rules — superadmin only.
|
||||
func (s *Server) handleCreateArchivingRule(w http.ResponseWriter, r *http.Request) {
|
||||
var body archivingRuleBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateArchivingRule(r.Context(), storage.ArchivingRule{
|
||||
TenantID: body.TenantID,
|
||||
CondType: body.CondType,
|
||||
Pattern: body.Pattern,
|
||||
Priority: body.Priority,
|
||||
RetentionDays: body.RetentionDays,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.auditRule(r, "archiving_rule_created", fmt.Sprintf("id=%d type=%s pattern=%s retention_days=%s",
|
||||
id, body.CondType, body.Pattern, retDaysStr(body.RetentionDays)))
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// handleUpdateArchivingRule updates an existing archiving rule.
|
||||
// PUT /api/admin/archiving-rules/{id} — superadmin only.
|
||||
func (s *Server) handleUpdateArchivingRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid rule id")
|
||||
return
|
||||
}
|
||||
var body archivingRuleBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
if err := s.store.UpdateArchivingRule(r.Context(), storage.ArchivingRule{
|
||||
ID: id,
|
||||
TenantID: body.TenantID,
|
||||
CondType: body.CondType,
|
||||
Pattern: body.Pattern,
|
||||
Priority: body.Priority,
|
||||
RetentionDays: body.RetentionDays,
|
||||
}); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.auditRule(r, "archiving_rule_updated", fmt.Sprintf("id=%d type=%s pattern=%s retention_days=%s",
|
||||
id, body.CondType, body.Pattern, retDaysStr(body.RetentionDays)))
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
||||
}
|
||||
|
||||
// handleDeleteArchivingRule deletes an archiving rule. Existing retain_until
|
||||
// values on already-archived mails are left unchanged (PROJ-51).
|
||||
// DELETE /api/admin/archiving-rules/{id} — superadmin only.
|
||||
func (s *Server) handleDeleteArchivingRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid rule id")
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteArchivingRule(r.Context(), id); err != nil {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.auditRule(r, "archiving_rule_deleted", fmt.Sprintf("id=%d", id))
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) auditRule(r *http.Request, event, detail string) {
|
||||
if s.audlog == nil {
|
||||
return
|
||||
}
|
||||
sess := sessionFromCtx(r.Context())
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: event,
|
||||
Username: sess.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: true,
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
|
||||
func retDaysStr(d *int) string {
|
||||
if d == nil {
|
||||
return "none"
|
||||
}
|
||||
return strconv.Itoa(*d)
|
||||
}
|
||||
Reference in New Issue
Block a user