package router import ( "context" "embed" "fmt" "io/fs" "log" "net/http" "opencatd-open/internal/api" "opencatd-open/internal/channel" "opencatd-open/internal/dao" "opencatd-open/internal/passkey" "opencatd-open/internal/proxy" "opencatd-open/internal/usage" "opencatd-open/middleware" "opencatd-open/pkg/config" "os" "os/signal" "sync" "syscall" "time" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) func SetRouter(cfg *config.Config, db *gorm.DB, web *embed.FS) { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup if cfg == nil || db == nil { panic("cfg or db is nil") } sqlDB, err := db.DB() if err != nil { log.Fatalf("Failed to get underlying *sql.DB: %v", err) } // Initialize DAOs userDAO := dao.NewUserDAO(db) apiKeyDAO := dao.NewApiKeyDAO(db) usageDAO := dao.NewUsageDAO(db) dailyDAO := dao.NewDailyUsageDAO(db) channelDAO := dao.NewChannelDAO(db) modelDAO := dao.NewModelDAO(db) // Initialize channel service channelSvc := channel.NewService(channelDAO, modelDAO) // Initialize health checker and start periodic checks healthChecker := channel.NewHealthChecker(channelDAO, channelSvc) go healthChecker.StartPeriodicCheck(ctx) // Initialize usage recorder and start background worker usageRecorder := usage.NewRecorder(usageDAO, dailyDAO) usageRecorder.Start(ctx) defer usageRecorder.Stop() // Initialize gateway gateway := proxy.NewGateway(ctx, cfg, db, &wg, userDAO, apiKeyDAO, usageDAO, dailyDAO) gateway.SetChannelService(channelSvc) gateway.SetUsageRecorder(usageRecorder) // Initialize passkey service var rdb *redis.Client if cfg.RedisHost != "" { rdb = redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", cfg.RedisHost, cfg.RedisPort), Password: cfg.RedisPassword, DB: cfg.RedisDB, }) } passkeySvc, err := passkey.New(db, passkey.Config{ RPID: cfg.RPID, Origins: cfg.RPOrigins, Name: cfg.AppName, Redis: rdb, }) if err != nil { log.Fatalf("Failed to initialize passkey service: %v", err) } // Initialize API handler apiHandler := api.NewHandler(db, passkeySvc) r := gin.Default() r.Use(middleware.CORS()) // Public auth routes public := r.Group("/api/auth") { public.POST("/register", apiHandler.Register) public.POST("/login", apiHandler.Login) public.POST("/passkey/begin", apiHandler.PasskeyLoginBegin) public.POST("/passkey/finish", apiHandler.PasskeyLoginComplete) } // API routes (authenticated) apiGroup := r.Group("/api", middleware.Auth(db)) { // User profile apiGroup.GET("/me", apiHandler.Me) apiGroup.GET("/profile", apiHandler.Me) apiGroup.POST("/profile/update", apiHandler.UpdateProfile) apiGroup.POST("/profile/update/password", apiHandler.UpdatePassword) // Passkey management apiGroup.POST("/webauthn/register/begin", apiHandler.PasskeyRegisterBegin) apiGroup.POST("/webauthn/register/complete", apiHandler.PasskeyRegisterComplete) apiGroup.GET("/webauthn/passkeys", apiHandler.PasskeyList) apiGroup.DELETE("/webauthn/passkeys/:id", apiHandler.PasskeyDelete) // User management (admin) apiGroup.GET("/users", apiHandler.ListUsers) apiGroup.GET("/users/:id", apiHandler.GetUser) apiGroup.POST("/users", apiHandler.CreateUser) apiGroup.PUT("/users/:id", apiHandler.UpdateUser) apiGroup.DELETE("/users/:id", apiHandler.DeleteUser) apiGroup.POST("/users/batch/:option", apiHandler.BatchUsers) // API Key management apiGroup.GET("/keys", apiHandler.ListApiKeys) apiGroup.GET("/keys/:id", apiHandler.GetApiKey) apiGroup.POST("/keys", apiHandler.CreateApiKey) apiGroup.PUT("/keys/:id", apiHandler.UpdateApiKey) apiGroup.DELETE("/keys/:id", apiHandler.DeleteApiKey) apiGroup.POST("/keys/batch/:option", apiHandler.BatchApiKeys) // Channel management (legacy endpoints) apiGroup.GET("/channels", apiHandler.ListChannels) apiGroup.POST("/channels", apiHandler.CreateChannel) apiGroup.PUT("/channels/:id", apiHandler.UpdateChannel) apiGroup.DELETE("/channels/:id", apiHandler.DeleteChannel) apiGroup.GET("/channels/:id/models", apiHandler.GetChannelModels) apiGroup.POST("/channels/:id/models", apiHandler.BindChannelModels) // Model management (legacy endpoints) apiGroup.GET("/models", apiHandler.ListModels) apiGroup.POST("/models", apiHandler.CreateModel) apiGroup.PUT("/models/:id", apiHandler.UpdateModel) apiGroup.DELETE("/models/:id", apiHandler.DeleteModel) // 用户自身用量统计 apiGroup.GET("/usage/stats", apiHandler.MyUsageStats) apiGroup.GET("/usage/monthly", apiHandler.MyUsageMonthly) apiGroup.GET("/usage/logs", apiHandler.MyUsageLogs) } // Admin API (requires admin role) adminGroup := r.Group("/api/admin", middleware.Auth(db), middleware.AdminOnly()) { // Admin channel management (enhanced) adminGroup.GET("/channels", apiHandler.AdminChannels) adminGroup.POST("/channels", apiHandler.AdminCreateChannel) adminGroup.PUT("/channels/:id", apiHandler.AdminUpdateChannel) adminGroup.DELETE("/channels/:id", apiHandler.AdminDeleteChannel) adminGroup.POST("/channels/:id/test", apiHandler.AdminTestChannel) adminGroup.GET("/channels/:id/models/remote", apiHandler.AdminChannelRemoteModels) adminGroup.GET("/channels/:id/models", apiHandler.AdminChannelModels) adminGroup.POST("/channels/:id/models", apiHandler.AdminChannelAddModel) adminGroup.PATCH("/channels/:id/models/:bid", apiHandler.AdminChannelUpdateModel) adminGroup.DELETE("/channels/:id/models/:bid", apiHandler.AdminChannelDeleteModel) // Admin model management (enhanced) adminGroup.GET("/models", apiHandler.AdminModels) adminGroup.DELETE("/models/unused", apiHandler.AdminDeleteUnusedModels) adminGroup.POST("/models", apiHandler.AdminCreateModel) adminGroup.PUT("/models/:id", apiHandler.AdminUpdateModel) adminGroup.DELETE("/models/:id", apiHandler.AdminDeleteModel) adminGroup.POST("/models/:id/bindings", apiHandler.AdminCreateModelBinding) adminGroup.DELETE("/models/:id/bindings/:bid", apiHandler.AdminDeleteModelBinding) // Admin system config adminGroup.GET("/config", apiHandler.AdminGetConfig) adminGroup.PUT("/config", apiHandler.AdminUpdateConfig) adminGroup.GET("/config/registration", apiHandler.AdminGetRegistration) adminGroup.PUT("/config/registration", apiHandler.AdminUpdateRegistration) adminGroup.GET("/config/password-login", apiHandler.AdminGetPasswordLogin) adminGroup.PUT("/config/password-login", apiHandler.AdminUpdatePasswordLogin) // Admin usage adminGroup.GET("/usage/logs", apiHandler.AdminUsageLogs) adminGroup.GET("/usage/summary", apiHandler.AdminUsageSummary) } // LLM proxy routes v1 := r.Group("/v1") v1.Use(middleware.AuthLLM(db)) { v1.POST("/chat/completions", gateway.HandleChat) v1.POST("/messages", gateway.HandleMessages) v1.POST("/responses", gateway.HandleResponses) v1.GET("/models", gateway.HandleModels) } // SPA fallback idxFS, err := fs.Sub(web, "dist") if err != nil { panic(err) } assetsFS, err := fs.Sub(web, "dist/assets") if err != nil { panic(err) } r.StaticFS("/assets", http.FS(assetsFS)) r.NoRoute(func(c *gin.Context) { if c.Writer.Status() == http.StatusNotFound { c.FileFromFS("/", http.FS(idxFS)) } }) srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), Handler: r, } go func() { fmt.Println("Starting server at port:", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit fmt.Println("\nShutdown Server ...") shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Fatalln("Server Shutdown:", err) } cancel() sqlDB.Close() waitChan := make(chan struct{}) go func() { wg.Wait() close(waitChan) }() select { case <-waitChan: fmt.Println("All goroutines have finished") case <-shutdownCtx.Done(): fmt.Println("⚠️ Shutdown timeout") } fmt.Println("Server exited") }