Files
scriptforge/backend/cmd/server/main.go
zhilv 5414c9c865 feat: 分类/变体体系 + 用户认证 + 管理后台
- 运行时分类体系:Shell/Python/JavaScript/Ruby/PHP 各含变体
- 用户注册/登录(JWT + bcrypt),首个注册用户为管理员
- 管理后台 /admin 动态管理分类和变体
- 脚本市场支持按分类筛选
- CodeMirror 语言模式根据分类名称自动切换
- 结果页展示该分类下所有变体的运行命令
- source 命令变体用于 Shell 类继承环境变量

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:02:20 +08:00

172 lines
4.1 KiB
Go

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))
})
}