// Package handler adapts application services to the HTTP protocol. package handler import ( "context" "go.uber.org/zap" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "git.sundynix.cn/Blizzard/Veyra/internal/adminui" "git.sundynix.cn/Blizzard/Veyra/internal/service" ) func NewRouter(health *service.HealthService, logger *zap.Logger, identity IdentityOptions) http.Handler { gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(func(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout(c.Request)) defer cancel() if identity.PublicOrigin != "" { ctx = context.WithValue(ctx, publicOriginContextKey{}, identity.PublicOrigin) } c.Request = c.Request.WithContext(ctx) requestID := uuid.NewString() c.Header("X-Request-ID", requestID) c.Header("X-Content-Type-Options", "nosniff") c.Header("Cache-Control", "no-store") start := time.Now() defer func() { if recover() != nil { logger.Error("request panic", zap.String("request_id", requestID)) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "INTERNAL_ERROR", "message": "Internal server error", "request_id": requestID}}) } route := c.FullPath() if route == "" { route = "/" if c.Request.URL.Path == "/" { route = "/" } else if strings.HasPrefix(c.Request.URL.Path, "/assets/") { route = "/assets/*" } } fields := []zap.Field{zap.String("method", c.Request.Method), zap.String("route", route), zap.Int("status", c.Writer.Status()), zap.Int64("duration_ms", time.Since(start).Milliseconds()), zap.String("request_id", requestID)} switch { case c.Writer.Status() >= 500: logger.Error("HTTP request", fields...) case c.Writer.Status() >= 400: logger.Warn("HTTP request", fields...) default: logger.Info("HTTP request", fields...) } }() c.Next() }) r.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "alive"}) }) check := func(c *gin.Context) { h := health.Check(c.Request.Context()) status := http.StatusOK if h.Status != "ready" { status = http.StatusServiceUnavailable } c.JSON(status, h) } r.GET("/readyz", check) r.GET("/api/v1/system/health", check) bindIdentity(r, identity) bindProviders(r, identity) bindCandidates(r, identity) bindCandidateAnalysis(r, identity) bindTradePlans(r, identity) bindTradeFills(r, identity) bindNotifications(r, identity) bindStrategies(r, identity) bindBacktests(r, identity) bindMarket(r, identity) console := adminui.Handler() r.NoRoute(func(c *gin.Context) { if !strings.HasPrefix(c.Request.URL.Path, "/api/") && c.Request.URL.Path != "/api" { console.ServeHTTP(c.Writer, c.Request) return } c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"code": "NOT_FOUND", "message": "Endpoint not found"}}) }) return r } func requestTimeout(r *http.Request) time.Duration { if r.Method == http.MethodGet && (r.URL.Path == "/api/admin/v1/market/history" || r.URL.Path == "/api/v1/market/history") || r.Method == http.MethodPost && r.URL.Path == "/api/admin/v1/backtests/from-market" { return 35 * time.Second } return 5 * time.Second }