133 lines
4.5 KiB
Go
133 lines
4.5 KiB
Go
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)
|
|
}
|