// Package middleware provides HTTP middleware for the BoosAPI server. package middleware import ( "log/slog" "net/http" "strings" "time" ) // Logger logs each HTTP request with method, path, status, and duration. func Logger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() sw := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(sw, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", sw.status, "duration", time.Since(start).Round(time.Millisecond).String(), "remote", r.RemoteAddr, ) }) } type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(status int) { w.status = status w.ResponseWriter.WriteHeader(status) } // AuthToken validates the AUTH_TOKEN header for admin endpoints. func AuthToken(token string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if token == "" { next.ServeHTTP(w, r) return } auth := r.Header.Get("Authorization") if auth != "Bearer "+token && auth != "Token "+token { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } } // ProxyToken validates the PROXY_TOKEN for /v1/* proxy endpoints. func ProxyToken(token string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if token == "" { next.ServeHTTP(w, r) return } auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { http.Error(w, `{"error":"missing bearer token"}`, http.StatusUnauthorized) return } if strings.TrimPrefix(auth, "Bearer ") != token { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } } // CORS adds CORS headers for the React frontend. func CORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } // Recover catches panics and returns a 500 error. func Recover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { slog.Error("panic recovered", "error", err) http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) }