From a631ac877090b16ea97f6ef4da533deed88c20c6 Mon Sep 17 00:00:00 2001 From: sysops Date: Fri, 28 Aug 2026 10:03:41 +0200 Subject: [PATCH] API-10: internal/apiserver-port wieder entfernen (ungenutzt, zieht internal/auth als fehlende abhaengigkeit nach) --- internal/apiserver/context.go | 20 ---- internal/apiserver/errors.go | 31 ------- internal/apiserver/middleware.go | 56 ----------- internal/apiserver/server.go | 37 -------- internal/apiserver/server_test.go | 149 ------------------------------ 5 files changed, 293 deletions(-) delete mode 100644 internal/apiserver/context.go delete mode 100644 internal/apiserver/errors.go delete mode 100644 internal/apiserver/middleware.go delete mode 100644 internal/apiserver/server.go delete mode 100644 internal/apiserver/server_test.go diff --git a/internal/apiserver/context.go b/internal/apiserver/context.go deleted file mode 100644 index 9ed7ca9..0000000 --- a/internal/apiserver/context.go +++ /dev/null @@ -1,20 +0,0 @@ -package apiserver - -import "context" - -type contextKey int - -const requestContextKey contextKey = iota - -// RequestContext ist der Tenant-/Benutzerkontext, den die Middleware-Kette -// fuer nachgelagerte Handler bereitstellt (Akzeptanzkriterium 3). -type RequestContext struct { - UserID string - TenantSlug string -} - -// FromContext liest den von der Middleware gesetzten Kontext. -func FromContext(ctx context.Context) (RequestContext, bool) { - rc, ok := ctx.Value(requestContextKey).(RequestContext) - return rc, ok -} diff --git a/internal/apiserver/errors.go b/internal/apiserver/errors.go deleted file mode 100644 index 2d952bf..0000000 --- a/internal/apiserver/errors.go +++ /dev/null @@ -1,31 +0,0 @@ -// Package apiserver implementiert Core API-01: das REST-Grundgerüst mit -// URL-Versionierung, einheitlichem Fehlerformat und Middleware-Kette -// (Auth, Tenant-/Benutzerkontext, Logging). -package apiserver - -import ( - "encoding/json" - "net/http" -) - -// errorBody ist das EINE Fehlerschema fuer alle Endpunkte unter /api/{version}/ -// (Akzeptanzkriterium 2). -type errorBody struct { - Error struct { - Code string `json:"code"` - Message string `json:"message"` - } `json:"error"` -} - -// WriteError schreibt einen Fehler im einheitlichen Schema. code ist ein -// stabiler, maschinenlesbarer Bezeichner (z.B. "unauthenticated"), message -// ein fuer Menschen lesbarer deutscher Text. -func WriteError(w http.ResponseWriter, status int, code, message string) { - var body errorBody - body.Error.Code = code - body.Error.Message = message - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(body) -} diff --git a/internal/apiserver/middleware.go b/internal/apiserver/middleware.go deleted file mode 100644 index 1587071..0000000 --- a/internal/apiserver/middleware.go +++ /dev/null @@ -1,56 +0,0 @@ -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)) - } -} diff --git a/internal/apiserver/server.go b/internal/apiserver/server.go deleted file mode 100644 index bd0c0a7..0000000 --- a/internal/apiserver/server.go +++ /dev/null @@ -1,37 +0,0 @@ -package apiserver - -import ( - "net/http" - - "gitea.perlbach24.de/scripte/nexarch/internal/auth" -) - -// Server registriert versionierte API-Routen (Akzeptanzkriterium 1: unter -// /api/{version}/...) und verdrahtet fuer jede Route dieselbe Middleware- -// Kette (Logging -> Auth+Tenantkontext -> Handler). -type Server struct { - mux *http.ServeMux - issuer *auth.TokenIssuer -} - -func NewServer(issuer *auth.TokenIssuer) *Server { - return &Server{mux: http.NewServeMux(), issuer: issuer} -} - -// Handle registriert pattern unter der angegebenen Version, z.B. -// Handle("v1", "/things", h) -> erreichbar unter /api/v1/things. Verschiedene -// Versionen sind unabhaengige Pfade — eine neue Version beeintraechtigt -// bestehende nicht (Akzeptanzkriterium 1 / Pruefung 3). -func (s *Server) Handle(version, pattern string, h http.HandlerFunc) { - full := "/api/" + version + pattern - s.mux.HandleFunc(full, loggingMiddleware(authAndTenantContext(s.issuer, h))) -} - -// HandleV1 ist die Kurzform fuer die aktuelle Hauptversion. -func (s *Server) HandleV1(pattern string, h http.HandlerFunc) { - s.Handle("v1", pattern, h) -} - -func (s *Server) Handler() http.Handler { - return s.mux -} diff --git a/internal/apiserver/server_test.go b/internal/apiserver/server_test.go deleted file mode 100644 index 6375976..0000000 --- a/internal/apiserver/server_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package apiserver - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "gitea.perlbach24.de/scripte/nexarch/internal/auth" -) - -func newTestServer() (*Server, *auth.TokenIssuer) { - issuer := auth.NewTokenIssuer("test-secret-nur-fuer-tests") - return NewServer(issuer), issuer -} - -func withAuthCookie(req *http.Request, token string) *http.Request { - req.AddCookie(&http.Cookie{Name: auth.CookieName, Value: token}) - return req -} - -// Akzeptanzkriterium 1: API unter versioniertem Pfad erreichbar. -func TestHandleV1_RegistersUnderVersionedPath(t *testing.T) { - srv, issuer := newTestServer() - srv.HandleV1("/things", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - token, err := issuer.Issue("user-1", "acme") - if err != nil { - t.Fatalf("issue: %v", err) - } - - req := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/things", nil), token) - rec := httptest.NewRecorder() - srv.Handler().ServeHTTP(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, want 200", rec.Code) - } -} - -// Akzeptanzkriterium 2 + Pruefung 1 (Stichprobe): mehrere Endpunkte liefern -// bei fehlerhafter Anfrage dasselbe Fehlerschema. -func TestErrorFormat_ConsistentAcrossEndpoints(t *testing.T) { - srv, _ := newTestServer() - srv.HandleV1("/endpunkt-a", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) - srv.HandleV1("/endpunkt-b", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) - - for _, path := range []string{"/api/v1/endpunkt-a", "/api/v1/endpunkt-b"} { - req := httptest.NewRequest(http.MethodGet, path, nil) // ohne cookie -> 401 - rec := httptest.NewRecorder() - srv.Handler().ServeHTTP(rec, req) - - if rec.Code != http.StatusUnauthorized { - t.Fatalf("%s: status = %d, want 401", path, rec.Code) - } - var body errorBody - if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { - t.Fatalf("%s: fehlerantwort nicht im erwarteten json-schema: %v (body: %s)", path, err, rec.Body.String()) - } - if body.Error.Code == "" || body.Error.Message == "" { - t.Fatalf("%s: erwartet nicht-leeren code/message, habe %+v", path, body) - } - } -} - -// Akzeptanzkriterium 3 + Pruefung 2: Middleware-Kette setzt Tenant-/ -// Benutzerkontext zuverlaessig, nachweislich fuer mehrere Endpunkte. -func TestMiddleware_SetsRequestContextForEveryEndpoint(t *testing.T) { - srv, issuer := newTestServer() - - var gotA, gotB RequestContext - srv.HandleV1("/kontext-a", func(w http.ResponseWriter, r *http.Request) { - gotA, _ = FromContext(r.Context()) - w.WriteHeader(http.StatusOK) - }) - srv.HandleV1("/kontext-b", func(w http.ResponseWriter, r *http.Request) { - gotB, _ = FromContext(r.Context()) - w.WriteHeader(http.StatusOK) - }) - - token, err := issuer.Issue("user-42", "tenant-x") - if err != nil { - t.Fatalf("issue: %v", err) - } - - for path, got := range map[string]*RequestContext{"/api/v1/kontext-a": &gotA, "/api/v1/kontext-b": &gotB} { - req := withAuthCookie(httptest.NewRequest(http.MethodGet, path, nil), token) - rec := httptest.NewRecorder() - srv.Handler().ServeHTTP(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("%s: status = %d, want 200", path, rec.Code) - } - if got.UserID != "user-42" || got.TenantSlug != "tenant-x" { - t.Fatalf("%s: request-context unerwartet: %+v", path, *got) - } - } -} - -// Akzeptanzkriterium 1 + Pruefung 3: eine neue v2-Route laesst sich anlegen, -// ohne v1 zu beeintraechtigen. -func TestVersioning_V2DoesNotAffectV1(t *testing.T) { - srv, issuer := newTestServer() - srv.HandleV1("/things", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("v1-antwort")) - }) - srv.Handle("v2", "/things", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("v2-antwort")) - }) - - token, err := issuer.Issue("user-1", "acme") - if err != nil { - t.Fatalf("issue: %v", err) - } - - reqV1 := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/things", nil), token) - recV1 := httptest.NewRecorder() - srv.Handler().ServeHTTP(recV1, reqV1) - if recV1.Body.String() != "v1-antwort" { - t.Fatalf("v1 antwort = %q, want v1-antwort", recV1.Body.String()) - } - - reqV2 := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v2/things", nil), token) - recV2 := httptest.NewRecorder() - srv.Handler().ServeHTTP(recV2, reqV2) - if recV2.Body.String() != "v2-antwort" { - t.Fatalf("v2 antwort = %q, want v2-antwort", recV2.Body.String()) - } - - // v1 nach dem Anlegen von v2 erneut pruefen — unveraendert. - reqV1Again := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/things", nil), token) - recV1Again := httptest.NewRecorder() - srv.Handler().ServeHTTP(recV1Again, reqV1Again) - if recV1Again.Body.String() != "v1-antwort" { - t.Fatalf("v1 antwort nach v2-anlage = %q, want weiterhin v1-antwort", recV1Again.Body.String()) - } -} - -func TestAuthAndTenantContext_RejectsInvalidToken(t *testing.T) { - srv, _ := newTestServer() - srv.HandleV1("/geschuetzt", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) - - req := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/geschuetzt", nil), "kaputtes.token.hier") - rec := httptest.NewRecorder() - srv.Handler().ServeHTTP(rec, req) - if rec.Code != http.StatusUnauthorized { - t.Fatalf("status = %d, want 401", rec.Code) - } -}