feat(archive): RET-06-API Aufbewahrungsfristen-Konfigurations-Backend

Board-Entscheidung: Backend-API zuerst, echtes Next.js-Frontend als
separates Folgeticket - vermeidet Pseudo-Frontend-Protokoll.
internal/retentionapi: 4 Endpunkte (anlegen/aendern, deaktivieren,
liste, vorschau), Vorschau nutzt dieselbe ListExpiringObjects-Funktion
wie RET-02s periodischer Job (keine Doppel-Implementierung).
RequireRole ist AUSDRUECKLICH kein RBAC-02-Ersatz, sondern ein
dokumentiertes Provisorium (Header-Check) - RBAC-02 ist reiner
Core-interner Go-Code ohne HTTP-Schnittstelle fuer andere Module,
derselbe Befund wie FDN-03/FDN-09. Provisorium real getestet inkl.
Negativfall (403 ohne/mit falscher Rolle). retention_class_rules um
active-Flag erweitert (deaktivieren ohne Historienverlust). Real auf
131 deployed und per curl end-to-end verifiziert.
This commit is contained in:
sysops
2026-08-30 02:09:37 +02:00
parent 2c9a7482b6
commit 21278f1405
11 changed files with 644 additions and 3 deletions
+98
View File
@@ -0,0 +1,98 @@
package retentionapi
import (
"encoding/json"
"net/http"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionengine"
)
const adminRole = "archive_admin"
// Mount registriert alle RET-06-API-Endpunkte auf mux, jeweils hinter
// dem provisorischen Rollen-Check (siehe authz.go) — Akzeptanzkriterium
// 3: Änderungen an Fristen sind nur berechtigten Rollen zugänglich.
func Mount(mux *http.ServeMux, pool *pgxpool.Pool) {
mux.HandleFunc("POST /retention-classes", RequireRole(adminRole, configureHandler(pool)))
mux.HandleFunc("POST /retention-classes/{class}/deactivate", RequireRole(adminRole, deactivateHandler(pool)))
mux.HandleFunc("GET /retention-classes", RequireRole(adminRole, listHandler(pool)))
mux.HandleFunc("GET /retention-classes/preview", RequireRole(adminRole, previewHandler(pool)))
}
type configureRequest struct {
RetentionClass string `json:"retention_class"`
Duration string `json:"duration"`
}
// configureHandler: Aufbewahrungsklasse anlegen ODER ändern
// (Akzeptanzkriterium 1) — `retentionengine.ConfigureClassRule` ist ein
// UPSERT, eine Änderung wirkt erst ab jetzt auf künftige
// Stichtagsberechnungen (Pflichtprüfung: nicht rückwirkend).
func configureHandler(pool *pgxpool.Pool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req configureRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungültiger request-body: "+err.Error(), http.StatusBadRequest)
return
}
if req.RetentionClass == "" || req.Duration == "" {
http.Error(w, "retention_class und duration sind pflichtfelder", http.StatusBadRequest)
return
}
if err := retentionengine.ConfigureClassRule(r.Context(), pool, req.RetentionClass, req.Duration); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}
// deactivateHandler: Aufbewahrungsklasse deaktivieren (Akzeptanzkriterium 1).
func deactivateHandler(pool *pgxpool.Pool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
class := r.PathValue("class")
if err := retentionengine.DeactivateClassRule(r.Context(), pool, class); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
}
}
// listHandler liefert alle konfigurierten Aufbewahrungsklassen (aktiv
// und deaktiviert) — Grundlage der künftigen Konfigurationsoberfläche.
func listHandler(pool *pgxpool.Pool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rules, err := retentionengine.ListClassRules(r.Context(), pool)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(rules)
}
}
// previewHandler liefert die Vorschauliste bald ablaufender Objekte
// (Akzeptanzkriterium 2: Standard 30 Tage, per `days`-Query-Parameter
// überschreibbar). Nutzt DIESELBE `ListExpiringObjects`-Funktion wie
// der periodische Job (RET-02) — Pflichtprüfung: Vorschauliste stimmt
// mit dem Ergebnis des periodischen Jobs überein (keine zweite,
// abweichende Implementierung).
func previewHandler(pool *pgxpool.Pool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
days := 30
asOf := time.Now().UTC().AddDate(0, 0, days)
objects, err := retentionengine.ListExpiringObjects(r.Context(), pool, asOf)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(objects)
}
}