internal/apiserver: Server.Handle(version, pattern, h) registriert Routen
unter /api/{version}/... (Akzeptanzkriterium 1) — verschiedene Versionen
sind unabhaengige Pfade im ServeMux, eine neue Version beeintraechtigt
bestehende nicht. HandleV1 ist die Kurzform fuer die aktuelle Hauptversion.
Einheitliches Fehlerschema {"error":{"code","message"}} ueber WriteError
(Akzeptanzkriterium 2) — bewusst NICHT auth.RequireAuth aus IAM-02
wiederverwendet, da dessen Klartext-Fehlerantworten nicht zum einheitlichen
JSON-Schema passen wuerden; stattdessen authAndTenantContext nutzt
auth.TokenIssuer.Verify direkt (dieselbe Kryptographie, keine Duplikation)
und antwortet im API-01-Schema, auch bei 401.
authAndTenantContext ist die Middleware, die JEDEM ueber Handle registrierten
Endpunkt Tenant-/Benutzerkontext bereitstellt (Akzeptanzkriterium 3, ueber
apiserver.FromContext abrufbar) — kein Handler prueft Auth selbst.
loggingMiddleware protokolliert jede Anfrage strukturiert.
Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Fehlerhafte Anfrage liefert ueber mehrere Endpunkte hinweg dasselbe
Fehlerschema (Stichprobe) — TestErrorFormat_ConsistentAcrossEndpoints:
zwei unabhaengige Endpunkte, beide liefern 401 im identischen
{"error":{"code","message"}}-Schema. PASS.
2. Middleware-Kette nachweislich von jedem Endpunkt durchlaufen —
TestMiddleware_SetsRequestContextForEveryEndpoint: zwei Endpunkte lesen
RequestContext, beide erhalten korrekten UserID/TenantSlug aus dem Token. PASS.
3. Versionswechsel (fiktive v2-Route) ohne v1 zu beeintraechtigen —
TestVersioning_V2DoesNotAffectV1: v1 vor und nach Anlage von v2 liefert
unveraendert dieselbe Antwort. PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
57 lines
1.8 KiB
Go
57 lines
1.8 KiB
Go
package apiserver
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
|
|
)
|
|
|
|
// authAndTenantContext prueft die Session (wiederverwendet auth.TokenIssuer.Verify
|
|
// aus IAM-02 — keine zweite JWT-Implementierung) und setzt bei Erfolg
|
|
// RequestContext fuer nachgelagerte Handler (Akzeptanzkriterium 3). Anders
|
|
// als auth.RequireAuth (Klartext-Fehler) antwortet diese Middleware im
|
|
// einheitlichen API-01-Fehlerschema (Akzeptanzkriterium 2), damit ALLE
|
|
// Endpunkte unter /api/{version}/ dasselbe Format liefern, auch bei
|
|
// Auth-Fehlern.
|
|
func authAndTenantContext(issuer *auth.TokenIssuer, next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
cookie, err := r.Cookie(auth.CookieName)
|
|
if err != nil {
|
|
WriteError(w, http.StatusUnauthorized, "unauthenticated", "nicht angemeldet")
|
|
return
|
|
}
|
|
|
|
claims, err := issuer.Verify(cookie.Value)
|
|
if err != nil {
|
|
WriteError(w, http.StatusUnauthorized, "unauthenticated", "nicht angemeldet")
|
|
return
|
|
}
|
|
|
|
rc := RequestContext{UserID: claims.UserID, TenantSlug: claims.TenantSlug}
|
|
next(w, r.WithContext(context.WithValue(r.Context(), requestContextKey, rc)))
|
|
}
|
|
}
|
|
|
|
type statusRecorder struct {
|
|
http.ResponseWriter
|
|
status int
|
|
}
|
|
|
|
func (s *statusRecorder) WriteHeader(code int) {
|
|
s.status = code
|
|
s.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// loggingMiddleware protokolliert jede Anfrage strukturiert.
|
|
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
|
start := time.Now()
|
|
next(rec, r)
|
|
slog.Info("api-anfrage", "method", r.Method, "path", r.URL.Path, "status", rec.status, "dauer", time.Since(start))
|
|
}
|
|
}
|