// Package httpserver assembles the gin engine (API routes + web UI) and runs it // with graceful shutdown. package httpserver import ( "context" "fmt" "net/http" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/kanootoko/codevault/internal/api" "github.com/kanootoko/codevault/internal/db" "github.com/kanootoko/codevault/internal/observability/logging" "github.com/kanootoko/codevault/internal/observability/metrics" "github.com/kanootoko/codevault/internal/ratelimit" "github.com/kanootoko/codevault/internal/utils/gshutdown" "github.com/kanootoko/codevault/internal/webui" ) const shutdownTimeout = 10 * time.Second const cleanupInterval = time.Minute func New( secrets *db.SecretsRepository, users *db.UsersRepository, limiter *ratelimit.Limiter, m metrics.Metrics, trustedProxies []string, ) (*gin.Engine, error) { router := gin.New() // Strict-server handlers only see context.Context (via *gin.Context, which // implements it), not *gin.Context directly. Without this flag, gin.Context.Value // never falls back to c.Request.Context(), so values stashed there by middleware // (e.g. LoginFromContext) would be invisible to handlers. router.ContextWithFallback = true router.Use(gin.Recovery()) if err := router.SetTrustedProxies(trustedProxies); err != nil { return nil, fmt.Errorf("error on setting trusted proxies: %w", err) } router.GET("/", func(c *gin.Context) { serveStatic(c, webui.GetPagePath) }) router.GET("/set", func(c *gin.Context) { serveStatic(c, webui.SetPagePath) }) if err := api.RegisterSwaggerHandlers(router); err != nil { return nil, fmt.Errorf("error on registering swagger handlers: %w", err) } handler := api.NewHandler(secrets, m.Value) strictHandler := api.NewStrictHandler(handler, nil) wrapper := api.ServerInterfaceWrapper{Handler: strictHandler} router.POST("/api/v1/getValue", api.InstrumentMiddleware("getValue", m.HTTP), api.RateLimitMiddleware(limiter, m.Value), wrapper.GetValue, ) router.POST("/api/v1/setValue", api.InstrumentMiddleware("setValue", m.HTTP), api.BasicAuthMiddleware(users, m.Value), wrapper.SetValue, ) return router, nil } func serveStatic(c *gin.Context, path string) { data, err := webui.Static.ReadFile(path) if err != nil { c.AbortWithStatus(http.StatusInternalServerError) return } c.Data(http.StatusOK, "text/html; charset=utf-8", data) } // Run starts the HTTP server and the rate limiter's cleanup goroutine, both // tracked as gshutdown subtasks, and blocks until ctx is done. func Run(ctx context.Context, address string, router *gin.Engine, limiter *ratelimit.Limiter) error { logger := logging.LoggerMustFromContext(ctx) server := &http.Server{ Addr: address, Handler: router, } _ = gshutdown.SubtaskStarted(ctx, "http-server") go func() { defer func() { _ = gshutdown.SubtaskFinished(ctx, "http-server") }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { logger.Error("error on http server shutdown", zap.Error(err)) } }() cleanupDone := make(chan struct{}) _ = gshutdown.SubtaskStarted(ctx, "ratelimit-cleanup") go func() { defer func() { _ = gshutdown.SubtaskFinished(ctx, "ratelimit-cleanup") }() limiter.Run(cleanupDone, cleanupInterval) }() go func() { <-ctx.Done() close(cleanupDone) }() logger.Info("starting http server", zap.String("address", address)) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { return fmt.Errorf("http server error: %w", err) } return nil }