Files
codevault/internal/httpserver/server.go
Aleksei Sokol 1ccf40396f Version 0.2.0
Changes:
- forbid to overwrite secrets of other users
- add some logs of success
- add validation on code (length 50)
- use same limiter for POST requests
2026-08-17 18:11:43 +03:00

118 lines
3.6 KiB
Go

// 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,
logger *zap.Logger,
) (*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, logger),
api.RateLimitMiddleware(limiter, m.Value),
wrapper.GetValue,
)
router.POST("/api/v1/setValue",
api.InstrumentMiddleware("setValue", m.HTTP, logger),
api.BasicAuthMiddleware(users, limiter, 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
}