b6ecd613d4
- Project structure: cmd/boosapi, internal/, pkg/ - Config loader (all env vars from Node.js original) - Database layer with SQLite/MySQL/Postgres support - Migration runner (sequential SQL files) - Health check endpoint (GET /healthz) - HTTP middleware: logger, auth, CORS, panic recovery - chi router with /admin, /v1, /api route groups - Graceful shutdown (SIGINT/SIGTERM) - Multi-stage Dockerfile (alpine, ~20MB image) - Initial schema migration (users, api_keys, sites, usage_logs) Signed-off-by: Boos4721 <boos4721@icloud.com>
103 lines
2.8 KiB
Go
103 lines
2.8 KiB
Go
// 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)
|
|
})
|
|
}
|