package main import ( "io/fs" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" "gitea.kmux.cn/zhilv/scriptforge/internal/assets" "gitea.kmux.cn/zhilv/scriptforge/internal/auth" "gitea.kmux.cn/zhilv/scriptforge/internal/handler" "gitea.kmux.cn/zhilv/scriptforge/internal/middleware" "gitea.kmux.cn/zhilv/scriptforge/internal/model" "gitea.kmux.cn/zhilv/scriptforge/internal/seed" "gitea.kmux.cn/zhilv/scriptforge/internal/service" ) func main() { gin.SetMode(gin.ReleaseMode) db := initDB() syncDB(db) seed.Run(db) svc := service.NewScriptService(db) authSvc := auth.NewAuthService(db, getJWTKey()) go cleanupLoop(svc) r := gin.New() r.Use(gin.Recovery(), middleware.Logger()) // Public API api := r.Group("/api") { api.Use(middleware.RateLimit(60)) api.POST("/scripts", middleware.RateLimit(10), handler.CreateScript(svc)) api.GET("/scripts/:id", handler.GetScript(svc)) api.POST("/scripts/:id/publish", handler.PublishScript(svc)) api.DELETE("/scripts/:id", handler.DeleteScript(svc)) api.GET("/market", handler.ListMarket(svc)) // Auth api.POST("/auth/register", handler.Register(authSvc)) api.POST("/auth/login", handler.Login(authSvc)) // Categories (public read) api.GET("/categories", handler.ListCategories(svc)) } // Authenticated API authGroup := r.Group("/api") authGroup.Use(middleware.JWTAuth(authSvc)) { authGroup.GET("/auth/me", handler.GetMe(authSvc)) } // Admin API adminGroup := r.Group("/api/admin") adminGroup.Use(middleware.JWTAuth(authSvc), middleware.AdminOnly(authSvc)) { adminGroup.GET("/categories", handler.ListCategories(svc)) adminGroup.POST("/categories", handler.CreateCategory(svc)) adminGroup.PUT("/categories/:id", handler.UpdateCategory(svc)) adminGroup.DELETE("/categories/:id", handler.DeleteCategory(svc)) adminGroup.POST("/categories/:id/variants", handler.CreateVariant(svc)) adminGroup.PUT("/variants/:id", handler.UpdateVariant(svc)) adminGroup.DELETE("/variants/:id", handler.DeleteVariant(svc)) } r.GET("/raw/:id", handler.GetRawScript(svc)) r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) serveStaticOrSPA(r) port := getPort() log.Printf("Listening on :%s", port) go func() { if err := r.Run(":" + port); err != nil { log.Fatalf("server error: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down...") } func initDB() *gorm.DB { dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "scriptforge.db" } db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) if err != nil { log.Fatalf("failed to connect database: %v", err) } return db } func syncDB(db *gorm.DB) { if err := db.AutoMigrate(&model.User{}, &model.RuntimeCategory{}, &model.RuntimeVariant{}, &model.Script{}); err != nil { log.Fatalf("failed to migrate database: %v", err) } } func cleanupLoop(svc *service.ScriptService) { for { time.Sleep(1 * time.Hour) if n, err := svc.CleanupExpired(); err != nil { log.Printf("cleanup error: %v", err) } else if n > 0 { log.Printf("cleaned up %d expired scripts", n) } } } func getJWTKey() string { key := os.Getenv("JWT_KEY") if key == "" { key = "scriptforge-dev-key-change-in-production" } return key } func getPort() string { if p := os.Getenv("PORT"); p != "" { return p } return "8080" } func serveStaticOrSPA(r *gin.Engine) { staticFS, err := fs.Sub(assets.FS, "dist") if err != nil { log.Printf("no embedded static files, API-only mode: %v", err) return } r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/raw") { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } file, err := staticFS.Open(path) if err != nil { c.FileFromFS("index.html", http.FS(staticFS)) return } file.Close() c.FileFromFS(path, http.FS(staticFS)) }) r.GET("/", func(c *gin.Context) { c.FileFromFS("index.html", http.FS(staticFS)) }) }