This commit is contained in:
2026-01-03 14:16:16 -08:00
commit 1f0e678d47
71 changed files with 16127 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
package server
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
// Server represents the HTTP server
type Server struct {
router *chi.Mux
logger *slog.Logger
addr string
httpServer *http.Server
}
// New creates a new HTTP server with chi router and CORS enabled
func New(addr string, logger *slog.Logger) *Server {
s := &Server{
router: chi.NewRouter(),
logger: logger,
addr: addr,
}
s.setupMiddleware()
return s
}
// setupMiddleware configures all middleware for the router
func (s *Server) setupMiddleware() {
// Request ID middleware
s.router.Use(middleware.RequestID)
// Real IP middleware
s.router.Use(middleware.RealIP)
// Rate limiting: 10 requests/second with burst of 30
// Generous limits since Cloudflare handles most protection
rateLimiter := NewRateLimiter(10, 30)
s.router.Use(rateLimiter.Middleware)
// Structured logging middleware
s.router.Use(s.loggingMiddleware)
// Recover from panics
s.router.Use(middleware.Recoverer)
// Request timeout
s.router.Use(middleware.Timeout(60 * time.Second))
// CORS configuration
s.router.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{
"https://paragliding.scottyah.com",
"http://localhost:3000",
"http://localhost:5173",
},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300,
}))
// Compress responses
s.router.Use(middleware.Compress(5))
}
// loggingMiddleware logs HTTP requests with structured logging
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
defer func() {
s.logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", ww.Status(),
"bytes", ww.BytesWritten(),
"duration", time.Since(start).String(),
"request_id", middleware.GetReqID(r.Context()),
)
}()
next.ServeHTTP(ww, r)
})
}
// Router returns the chi router
func (s *Server) Router() *chi.Mux {
return s.router
}
// Start starts the HTTP server
func (s *Server) Start() error {
s.logger.Info("starting HTTP server", "addr", s.addr)
s.httpServer = &http.Server{
Addr: s.addr,
Handler: s.router,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
}
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("server failed: %w", err)
}
return nil
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
s.logger.Info("shutting down HTTP server")
if s.httpServer == nil {
return nil
}
return s.httpServer.Shutdown(ctx)
}