package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" _ "github.com/ez-api/ez-api/docs" "github.com/ez-api/ez-api/internal/api" "github.com/ez-api/ez-api/internal/config" "github.com/ez-api/ez-api/internal/middleware" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" "github.com/ez-api/foundation/logging" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "gorm.io/driver/postgres" "gorm.io/gorm" ) // @title EZ-API Control Plane // @version 0.0.1 // @description Management API for EZ-API Gateway system. // @termsOfService http://swagger.io/terms/ // @contact.name API Support // @contact.url http://www.swagger.io/support // @contact.email support@swagger.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8080 // @BasePath / // @securityDefinitions.apikey AdminAuth // @in header // @name Authorization // @securityDefinitions.apikey MasterAuth // @in header // @name Authorization func fatal(logger *slog.Logger, msg string, args ...any) { logger.Error(msg, args...) os.Exit(1) } func main() { logger, _ := logging.New(logging.Options{Service: "ez-api"}) // 1. Load Configuration cfg, err := config.Load() if err != nil { fatal(logger, "failed to load config", "err", err) } // 2. Initialize Redis Client rdb := redis.NewClient(&redis.Options{ Addr: cfg.Redis.Addr, Password: cfg.Redis.Password, DB: cfg.Redis.DB, }) // Verify Redis connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := rdb.Ping(ctx).Err(); err != nil { fatal(logger, "failed to connect to redis", "err", err) } logger.Info("connected to redis successfully") // 3. Initialize GORM (PostgreSQL) db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{}) if err != nil { fatal(logger, "failed to connect to postgresql", "err", err) } sqlDB, err := db.DB() if err != nil { fatal(logger, "failed to get generic database object", "err", err) } // Verify DB connection if err := sqlDB.Ping(); err != nil { fatal(logger, "failed to ping postgresql", "err", err) } logger.Info("connected to postgresql successfully") // Auto Migrate if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.Provider{}, &model.Model{}, &model.Binding{}, &model.LogRecord{}); err != nil { fatal(logger, "failed to auto migrate", "err", err) } // 4. Setup Services and Handlers syncService := service.NewSyncService(rdb) logWriter := service.NewLogWriter(db, cfg.Log.QueueCapacity, cfg.Log.BatchSize, cfg.Log.FlushInterval) logCtx, cancelLogs := context.WithCancel(context.Background()) defer cancelLogs() logWriter.Start(logCtx) adminService, err := service.NewAdminService() if err != nil { fatal(logger, "failed to create admin service", "err", err) } masterService := service.NewMasterService(db) healthService := service.NewHealthCheckService(db, rdb) handler := api.NewHandler(db, syncService, logWriter) adminHandler := api.NewAdminHandler(masterService, syncService) masterHandler := api.NewMasterHandler(masterService, syncService) featureHandler := api.NewFeatureHandler(rdb) // 4.1 Prime Redis snapshots so DP can start with data if err := syncService.SyncAll(db); err != nil { logger.Warn("initial sync warning", "err", err) } // 5. Setup Gin Router r := gin.Default() r.Use(middleware.RequestID()) // CORS Middleware r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // TODO: Restrict this in production c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() }) // Health Check Endpoint r.GET("/health", func(c *gin.Context) { status := healthService.Check(c.Request.Context()) httpStatus := http.StatusOK if status.Status == "down" { httpStatus = http.StatusServiceUnavailable } c.JSON(httpStatus, status) }) // Swagger Documentation r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // API Routes // Admin Routes adminGroup := r.Group("/admin") adminGroup.Use(middleware.AdminAuthMiddleware(adminService)) { adminGroup.POST("/masters", adminHandler.CreateMaster) adminGroup.POST("/masters/:id/keys", adminHandler.IssueChildKeyForMaster) adminGroup.GET("/features", featureHandler.ListFeatures) adminGroup.PUT("/features", featureHandler.UpdateFeatures) // Other admin routes for managing providers, models, etc. adminGroup.POST("/providers", handler.CreateProvider) adminGroup.PUT("/providers/:id", handler.UpdateProvider) adminGroup.POST("/models", handler.CreateModel) adminGroup.GET("/models", handler.ListModels) adminGroup.PUT("/models/:id", handler.UpdateModel) adminGroup.POST("/bindings", handler.CreateBinding) adminGroup.GET("/bindings", handler.ListBindings) adminGroup.PUT("/bindings/:id", handler.UpdateBinding) adminGroup.POST("/sync/snapshot", handler.SyncSnapshot) } // Master Routes masterGroup := r.Group("/v1") masterGroup.Use(middleware.MasterAuthMiddleware(masterService)) { masterGroup.POST("/tokens", masterHandler.IssueChildKey) } // Public/General Routes (if any) r.POST("/logs", handler.IngestLog) srv := &http.Server{ Addr: ":" + cfg.Server.Port, Handler: r, } // 6. Start Server with Graceful Shutdown go func() { logger.Info("starting ez-api", "port", cfg.Server.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { fatal(logger, "server failed", "err", err) } }() // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info("shutting down server...") // Shutdown with timeout ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { fatal(logger, "server forced to shutdown", "err", err) } logger.Info("server exited properly") }