Files
zeekay 114b9701db refactor: /v1/ canonical paths (sweep)
Per global rule: /v1/ only, never /api/. Internal API calls + handler
comments + route registrations updated.

- Control plane: agentAPI mount moves from /api/v1 -> /v1; uiAPI from
  /api/ui/v1 -> /v1/ui; uiAPIV2 from /api/ui/v2 -> /v2/ui. Auth middleware
  /api/v1/health -> /v1/health. All UI handler comments updated.
- Go SDK (sdk/go/agent, sdk/go/client): control_plane_memory_backend,
  discovery, agent, note, client now use /v1/* and /v1/ui/* canonically.
- LegacyExecutionEndpoints catalog in pkg/types/execution.go normalized
  to /v1/* and /v1/ui/* (UIWorkflowRunList stays /v2/ui as it's the v2 surface).
- All test fixtures updated to match the new contract.

Build note: pre-existing rot in internal/utils/paths.go and
internal/handlers/ui/env.go (hanzo-agentsHome is not a valid Go identifier;
hyphens not allowed) prevents `go build ./...` from succeeding — this
predates the sweep and is not introduced here.
2026-05-18 22:34:48 -07:00

78 lines
1.7 KiB
Go

package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// AuthConfig mirrors server configuration for HTTP authentication.
type AuthConfig struct {
APIKey string
SkipPaths []string
}
// APIKeyAuth enforces API key authentication via header, bearer token, or query param.
func APIKeyAuth(config AuthConfig) gin.HandlerFunc {
skipPathSet := make(map[string]struct{}, len(config.SkipPaths))
for _, p := range config.SkipPaths {
skipPathSet[p] = struct{}{}
}
return func(c *gin.Context) {
// No auth configured, allow everything.
if config.APIKey == "" {
c.Next()
return
}
// Skip explicit paths
if _, ok := skipPathSet[c.Request.URL.Path]; ok {
c.Next()
return
}
// Always allow health and metrics by default
if strings.HasPrefix(c.Request.URL.Path, "/v1/health") || c.Request.URL.Path == "/health" || c.Request.URL.Path == "/metrics" {
c.Next()
return
}
// Allow UI static files to load (the React app handles auth prompting)
// Also allow root "/" which redirects to /ui/
if strings.HasPrefix(c.Request.URL.Path, "/ui") || c.Request.URL.Path == "/" {
c.Next()
return
}
apiKey := ""
// Preferred: X-API-Key header
apiKey = c.GetHeader("X-API-Key")
// Fallback: Authorization: Bearer <token>
if apiKey == "" {
authHeader := c.GetHeader("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
apiKey = strings.TrimPrefix(authHeader, "Bearer ")
}
}
// SSE/WebSocket friendly: api_key query parameter
if apiKey == "" {
apiKey = c.Query("api_key")
}
if apiKey != config.APIKey {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"message": "invalid or missing API key",
})
return
}
c.Next()
}
}