fetch_ml/internal/api/middleware/validation.go
Jeremie Fraeys 7e5ceec069
feat(api): add groups and tokens handlers, refactor routes
Add new API endpoints and clean up handler interfaces:

- groups/handlers.go: New lab group management API
  * CRUD operations for lab groups
  * Member management with role assignment (admin/member/viewer)
  * Group listing and membership queries

- tokens/handlers.go: Token generation and validation endpoints
  * Create access tokens for public task sharing
  * Validate tokens for secure access
  * Token revocation and cleanup

- routes.go: Refactor handler registration
  * Integrate groups handler into WebSocket routes
  * Remove nil parameters from all handler constructors
  * Cleaner dependency injection pattern

- Handler interface cleanup across all modules:
  * jobs/handlers.go: Remove unused nil privacyEnforcer parameter
  * jupyter/handlers.go: Streamline initialization
  * scheduler/handlers.go: Consistent constructor signature
  * ws/handler.go: Add groups handler to dependencies
2026-03-08 12:51:25 -04:00

90 lines
2.4 KiB
Go

// Package middleware provides request/response validation using OpenAPI spec
package middleware
import (
"bytes"
"encoding/json"
"io"
"net/http"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers"
"github.com/getkin/kin-openapi/routers/gorillamux"
)
// ValidationMiddleware validates HTTP requests against OpenAPI spec
type ValidationMiddleware struct {
router routers.Router
}
// NewValidationMiddleware creates a new validation middleware from OpenAPI spec
func NewValidationMiddleware(specPath string) (*ValidationMiddleware, error) {
// Load OpenAPI spec
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(specPath)
if err != nil {
return nil, err
}
// Validate the spec itself
if err := doc.Validate(loader.Context); err != nil {
return nil, err
}
// Create router for path matching
router, err := gorillamux.NewRouter(doc)
if err != nil {
return nil, err
}
return &ValidationMiddleware{router: router}, nil
}
// ValidateRequest validates an incoming HTTP request
func (v *ValidationMiddleware) ValidateRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip validation for health endpoints
if r.URL.Path == "/health/ok" || r.URL.Path == "/health" || r.URL.Path == "/metrics" {
next.ServeHTTP(w, r)
return
}
// Find the route
route, pathParams, err := v.router.FindRoute(r)
if err != nil {
// Route not in spec - allow through (might be unregistered endpoint)
next.ServeHTTP(w, r)
return
}
// Read and restore body for validation
var bodyBytes []byte
if r.Body != nil && r.Body != http.NoBody {
bodyBytes, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
// Validate request - body is read from r.Body automatically
requestValidationInput := &openapi3filter.RequestValidationInput{
Request: r,
PathParams: pathParams,
Route: route,
}
if err := openapi3filter.ValidateRequest(r.Context(), requestValidationInput); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
if encodeErr := json.NewEncoder(w).Encode(map[string]any{
"error": "validation failed",
"message": err.Error(),
}); encodeErr != nil {
// Log but don't return - we've already sent headers
_ = encodeErr
}
return
}
next.ServeHTTP(w, r)
})
}