package serviceadapter import ( "fmt" "net/http" "forgejo.riotpiao.com/rock/homelab-frontend/internal/problem" ) // Dispatcher handles X-Service based routing to service adapters. type Dispatcher struct { registry *Registry // authValidator would check capabilities if internal/auth exists // For now, we stub it } // NewDispatcher creates a new service adapter dispatcher. func NewDispatcher(registry *Registry) *Dispatcher { return &Dispatcher{ registry: registry, } } // Matches returns true if the request should be dispatched based on X-Service header. func (d *Dispatcher) Matches(r *http.Request) bool { return r.Header.Get("X-Service") != "" } // Dispatch routes a request to the appropriate adapter. // Returns a problem document if the adapter or resource is not found. func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) { serviceName := r.Header.Get("X-Service") if serviceName == "" { // No X-Service header — this shouldn't happen if Matches() was called d.writeError(w, problem.BadRequest("X-Service header required")) return } // Look up service adapter adapter := d.registry.Get(serviceName) if adapter == nil { p := problem.NotFound(fmt.Sprintf("service '%s' not found", serviceName)) _ = p.Write(w) return } // Get resource and method from request resourceName := r.Header.Get("X-Resource") if resourceName == "" { d.writeError(w, problem.BadRequest("X-Resource header required")) return } // Find resource var resource *Resource for i := range adapter.Spec.Resources { if adapter.Spec.Resources[i].Name == resourceName { resource = &adapter.Spec.Resources[i] break } } if resource == nil { p := problem.NotFound(fmt.Sprintf("resource '%s' not found in service '%s'", resourceName, serviceName)) _ = p.Write(w) return } // Find method matching HTTP verb var method *Method for i := range resource.Methods { if resource.Methods[i].Verb == r.Method { method = &resource.Methods[i] break } } if method == nil { p := problem.NotFound(fmt.Sprintf("method %s not defined for resource '%s'", r.Method, resourceName)) _ = p.Write(w) return } // Check auth requirements (stub for now — internal/auth integration in 8.3) // Determine required capability requiredCapability := "" auth := resource.Auth if auth == nil { auth = &adapter.Spec.Auth } if method.Auth != nil { auth = method.Auth } if auth != nil && auth.Required && auth.Capability != "" { requiredCapability = auth.Capability // Would validate JWT and capability here (depends on internal/auth) // For now, stub — just log that it would be checked if !d.hasCapability(r, requiredCapability) { p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden", "Forbidden", fmt.Sprintf("capability '%s' required", requiredCapability)) _ = p.Write(w) return } } // TODO: Call upstream with method.UpstreamPath, apply retry logic, etc. // For now, just echo that dispatch would happen w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"service":"%s","resource":"%s","method":"%s","upstream":"%s"}`, serviceName, resourceName, r.Method, adapter.Spec.Upstream.URL) } // hasCapability checks if the request has the required capability. // Stub implementation — depends on internal/auth JWT validation. func (d *Dispatcher) hasCapability(r *http.Request, capability string) bool { // TODO: Parse JWT from Authorization header and check capabilities // For now, assume all authenticated requests have all capabilities return r.Header.Get("Authorization") != "" } func (d *Dispatcher) writeError(w http.ResponseWriter, p *problem.Problem) { _ = p.Write(w) }