package resync import ( "context" "encoding/json" "net/http" "time" ) // AuditRecorder ist die schmale Schnittstelle, ueber die Core empfangene // Audit-Events tatsaechlich persistiert. In Produktion durch internal/audit // (AUD-01, nicht Abhaengigkeit dieser Kachel) implementiert — dieses Paket // dupliziert dessen Validierungs-/Speicherlogik NICHT, sondern ruft sie nur // auf. Die Events werden vom Aufrufer sequenziell in PendingAuditEvents- // Reihenfolge uebergeben (Akzeptanzkriterium 2). type AuditRecorder interface { Record(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any, occurredAt time.Time) error } // UsageIncrementer ist die schmale Schnittstelle zu Core's Nutzungszaehler // (in Produktion internal/usage, LIC-03 — nicht Abhaengigkeit dieser // Kachel). Jedes gepufferte Delta wird GENAU EINMAL angewendet. type UsageIncrementer interface { Increment(ctx context.Context, tenantSlug, metric string, delta int64) error } // CredentialAuthenticator ist die schmale Schnittstelle zu API-02s // Service-Credential-Pruefung (internal/moduleregistry.Registry.Authenticate). type CredentialAuthenticator interface { Authenticate(ctx context.Context, clientID, secret string) (moduleName string, ok bool, err error) } // Handler nimmt nachgelieferte Audit-Events/Nutzungsdeltas auf der // Core-Seite entgegen — authentifiziert ueber dasselbe Service-Credential // wie jeder andere Modul-Core-Aufruf (Akzeptanzkriterium 2/3, "authentifiziert // ueber das in API-02 definierte Service-Credential"). type Handler struct { auth CredentialAuthenticator audit AuditRecorder usage UsageIncrementer } func NewHandler(auth CredentialAuthenticator, audit AuditRecorder, usage UsageIncrementer) *Handler { return &Handler{auth: auth, audit: audit, usage: usage} } type credentialHeader struct { ClientID string `json:"client_id"` Secret string `json:"secret"` } func (h *Handler) authenticate(w http.ResponseWriter, r *http.Request) bool { clientID := r.Header.Get("X-Nexarch-Client-Id") secret := r.Header.Get("X-Nexarch-Client-Secret") _, ok, err := h.auth.Authenticate(r.Context(), clientID, secret) if err != nil || !ok { http.Error(w, "ungueltiges service-credential", http.StatusUnauthorized) return false } return true } type auditEventDTO struct { TenantSlug string `json:"tenant_slug"` Actor string `json:"actor"` Action string `json:"action"` Target string `json:"target"` Metadata map[string]any `json:"metadata"` OccurredAt time.Time `json:"occurred_at"` } // AuditHandler nimmt EINE Liste gepufferter Audit-Events entgegen und // schreibt sie SEQUENZIELL in der gegebenen Reihenfolge fort // (Akzeptanzkriterium 2 / Pruefung 1: Vollstaendigkeit + Reihenfolge). // Bricht die Verarbeitung bei einem Fehler ab und meldet, wie viele Events // bereits sicher geschrieben wurden — der Aufrufer (Worker) entfernt aus // seinem lokalen Puffer NUR die bestaetigt geschriebenen Events. func (h *Handler) AuditHandler(w http.ResponseWriter, r *http.Request) { if !h.authenticate(w, r) { return } var events []auditEventDTO if err := json.NewDecoder(r.Body).Decode(&events); err != nil { http.Error(w, "ungueltiger anfrage-koerper", http.StatusBadRequest) return } written := 0 for _, e := range events { if err := h.audit.Record(r.Context(), e.TenantSlug, e.Actor, e.Action, e.Target, e.Metadata, e.OccurredAt); err != nil { break } written++ } writeJSON(w, map[string]int{"written": written}) } type usageDeltaDTO struct { TenantSlug string `json:"tenant_slug"` Metric string `json:"metric"` Delta int64 `json:"delta"` } // UsageHandler wendet JEDES gepufferte Delta GENAU EINMAL an // (Akzeptanzkriterium 3 / Pruefung 2: keine Doppelzaehlung) — der Aufrufer // entfernt aus seinem lokalen Puffer nur die bestaetigt uebernommenen Deltas. func (h *Handler) UsageHandler(w http.ResponseWriter, r *http.Request) { if !h.authenticate(w, r) { return } var deltas []usageDeltaDTO if err := json.NewDecoder(r.Body).Decode(&deltas); err != nil { http.Error(w, "ungueltiger anfrage-koerper", http.StatusBadRequest) return } applied := 0 for _, d := range deltas { if err := h.usage.Increment(r.Context(), d.TenantSlug, d.Metric, d.Delta); err != nil { break } applied++ } writeJSON(w, map[string]int{"applied": applied}) } func writeJSON(w http.ResponseWriter, body any) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(body) }