// Package api implements HTTP middleware for the dashboard. package api import ( "fmt" "net/http" "runtime" "time" "git.ardenone.com/jedarden/zai-proxy/dashboard/logger" ) // Middleware is a function that wraps an http.Handler. type Middleware func(http.Handler) http.Handler // Chain applies multiple middleware in order. func Chain(handler http.Handler, middlewares ...Middleware) http.Handler { for i := len(middlewares) - 1; i >= 0; i-- { handler = middlewares[i](handler) } return handler } // LoggingMiddleware logs all requests with structured JSON logging. func LoggingMiddleware(next http.Handler) http.Handler { log := logger.Component("http") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Create response wrapper to capture status code rw := &responseWriter{ResponseWriter: w, status: http.StatusOK} // Process request next.ServeHTTP(rw, r) // Calculate duration duration := time.Since(start) // Log the request log.Info("request", "method", r.Method, "path", r.URL.Path, "status", rw.status, "duration_ms", duration.Milliseconds(), "remote_addr", r.RemoteAddr, "user_agent", r.UserAgent(), "content_length", r.ContentLength, ) }) } // RecoveryMiddleware recovers from panics and logs them. func RecoveryMiddleware(next http.Handler) http.Handler { log := logger.Component("http") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { // Get stack trace buf := make([]byte, 4096) n := runtime.Stack(buf, false) stackTrace := string(buf[:n]) log.Error("panic recovered", "error", fmt.Sprintf("%v", err), "path", r.URL.Path, "method", r.Method, "stack", stackTrace, ) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } // CORSMiddleware adds CORS headers to responses. func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") // Handle preflight request if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } // responseWriter wraps http.ResponseWriter to capture status code. type responseWriter struct { http.ResponseWriter status int } func (rw *responseWriter) WriteHeader(statusCode int) { rw.status = statusCode rw.ResponseWriter.WriteHeader(statusCode) } // Flush implements http.Flusher, required for SSE streaming. func (rw *responseWriter) Flush() { if f, ok := rw.ResponseWriter.(http.Flusher); ok { f.Flush() } }