Traefik_Control/backend/internal/api/server.go

175 lines
No EOL
5.7 KiB
Go

package api
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik-gui/backend/internal/api/handlers"
"github.com/traefik/traefik-gui/backend/internal/api/middleware"
"github.com/traefik/traefik-gui/backend/internal/config"
"github.com/traefik/traefik-gui/backend/internal/config/file"
"github.com/traefik/traefik-gui/backend/internal/database"
"github.com/traefik/traefik-gui/backend/internal/database/repositories"
"github.com/traefik/traefik-gui/backend/internal/traefik"
)
type Server struct {
httpServer *http.Server
engine *gin.Engine
config *config.Config
db *database.DB
traefik traefik.TraefikAPI
}
func NewServer(cfg *config.Config, db *database.DB, traefikAPI traefik.TraefikAPI) *Server {
if !cfg.DevMode {
gin.SetMode(gin.ReleaseMode)
}
engine := gin.New()
if len(cfg.TrustedProxies) > 0 {
engine.SetTrustedProxies(cfg.TrustedProxies)
}
userRepo := repositories.NewUserRepository(db.DB)
sessionRepo := repositories.NewSessionRepository(db.DB)
authMiddleware := middleware.NewAuthMiddleware(sessionRepo, userRepo)
healthHandler := handlers.NewHealthHandler()
authHandler := handlers.NewAuthHandler(
userRepo,
sessionRepo,
cfg.SessionSecret,
"", // cookie domain
!cfg.DevMode, // cookie secure - true in prod
)
// Legacy adapter for old ConfigHandler (which expects TraefikClient returning models)
legacyTraefik := traefik.NewLegacyAdapter(traefikAPI)
configHandler := handlers.NewConfigHandler(legacyTraefik)
traefikHandler := handlers.NewTraefikAPIHandler(traefikAPI)
// File-provider service (Phase 2)
auditRepo := repositories.NewAuditRepository(db.DB)
fileSvc, err := file.NewService(cfg.ConfigDir, db.DB, auditRepo)
if err != nil {
log.Fatal().Err(err).Str("configDir", cfg.ConfigDir).Msg("Failed to init file service")
}
fileHandler := handlers.NewFileConfigHandler(fileSvc, auditRepo, userRepo)
// Enhance health ready to check config dir writable
healthHandler.SetConfigDir(cfg.ConfigDir)
// Middleware
engine.Use(middleware.LoggingMiddleware())
engine.Use(middleware.SecurityHeadersMiddleware())
engine.Use(middleware.CORSMiddleware(cfg.CORSOrigin))
engine.Use(gin.Recovery())
// Health endpoints (no auth)
engine.GET("/api/health", healthHandler.Health)
engine.GET("/api/ready", healthHandler.Ready)
// Auth endpoints
authGroup := engine.Group("/api/auth")
{
authGroup.POST("/login", authHandler.Login)
authGroup.POST("/logout", authMiddleware.RequireAuth(), authMiddleware.RequireCSRF(), authHandler.Logout)
authGroup.GET("/me", authMiddleware.RequireAuth(), authHandler.Me)
authGroup.GET("/csrf", authMiddleware.RequireAuth(), authHandler.GetCSRF)
}
// Protected API endpoints
apiGroup := engine.Group("/api")
apiGroup.Use(authMiddleware.RequireAuth())
apiGroup.Use(authMiddleware.RequireCSRF())
{
// Config endpoints (file-provider, Phase 2)
configGroup := apiGroup.Group("/config")
{
// File-provider management (secure, atomic, validated)
configGroup.GET("/files", fileHandler.ListFiles)
configGroup.GET("/files/:name", fileHandler.GetFile)
configGroup.GET("/history", fileHandler.History)
configGroup.POST("/preview", fileHandler.Preview)
configGroup.POST("/validate", fileHandler.Validate)
configGroup.POST("/apply", fileHandler.Apply)
configGroup.POST("/rollback", fileHandler.Rollback)
// Legacy mock endpoints (read-only dashboard, kept for compatibility)
configGroup.GET("/routers", configHandler.ListRouters)
configGroup.GET("/routers/:id", configHandler.GetRouter)
configGroup.POST("/routers", configHandler.CreateRouter)
configGroup.PUT("/routers/:id", configHandler.UpdateRouter)
configGroup.DELETE("/routers/:id", configHandler.DeleteRouter)
configGroup.GET("/services", configHandler.ListServices)
configGroup.GET("/middlewares", configHandler.ListMiddlewares)
configGroup.GET("/certificates", configHandler.ListCertificates)
configGroup.GET("/entrypoints", configHandler.ListEntryPoints)
}
// Traefik status endpoints (read-only, new typed client with cache, viewer+)
traefikGroup := engine.Group("/api/traefik")
traefikGroup.Use(authMiddleware.RequireAuth())
{
traefikGroup.GET("/health", traefikHandler.Health)
traefikGroup.GET("/overview", traefikHandler.Overview)
traefikGroup.GET("/entrypoints", traefikHandler.Entrypoints)
traefikGroup.GET("/routers", traefikHandler.Routers)
traefikGroup.GET("/services", traefikHandler.Services)
traefikGroup.GET("/middlewares", traefikHandler.Middlewares)
traefikGroup.GET("/providers", traefikHandler.Providers)
traefikGroup.GET("/certificates", traefikHandler.Certificates)
}
}
// Serve embedded frontend in production
if !cfg.DevMode {
// TODO: Embed frontend assets
engine.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
})
}
srv := &Server{
engine: engine,
config: cfg,
db: db,
traefik: traefikAPI,
httpServer: &http.Server{
Addr: cfg.Addr,
Handler: engine,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
},
}
return srv
}
func (s *Server) Start(ctx context.Context) error {
log.Info().Str("addr", s.config.Addr).Msg("Starting HTTP server")
go func() {
<-ctx.Done()
log.Info().Msg("Shutting down HTTP server")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.httpServer.Shutdown(shutdownCtx)
}()
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}