package main import ( "embed" "io/fs" "net/http" "os" "strings" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "flowstack/internal/database" "flowstack/internal/handler" ) // 前端产物在构建期拷贝到 static/ 并嵌入二进制(run.sh build) // //go:embed all:static var staticFS embed.FS // 使用指南与接口文档(运行目录 docs/ 优先) // //go:embed docs/GUIDE.md docs/API.md var docsFS embed.FS func main() { if err := database.Init(); err != nil { panic("数据库初始化失败: " + err.Error()) } hub := handler.NewHub() r := gin.Default() // 会话鉴权:所有 /api 需登录(豁免登录/健康检查/入站 Webhook/SSE 日志流) r.Use(handler.AuthMiddleware()) // ?_clear=1 时下发 Clear-Site-Data,用于清除浏览器缓存的错误重定向等脏缓存 // (历史 bug:/index.html 曾返回 301 → ./,浏览器永久缓存后反复触发 ERR_TOO_MANY_REDIRECTS) r.Use(func(c *gin.Context) { if c.Query("_clear") != "" { c.Header("Clear-Site-Data", "\"cache\"") } c.Next() }) r.Use(cors.New(cors.Config{ AllowAllOrigins: true, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"*"}, AllowCredentials: false, })) r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok", "app": "flowstack"}) }) api := r.Group("/api") { api.POST("/auth/login", handler.Login) api.POST("/auth/logout", func(c *gin.Context) { c.JSON(200, gin.H{"success": true}) }) api.GET("/me", handler.Me) // 用户管理(列表登录即可读——权限成员选择器需要;增删改仅管理员) api.GET("/users", handler.ListUsers) users := api.Group("/users", handler.AdminOnly()) { users.POST("", handler.CreateUser) users.PUT("/:id", handler.UpdateUser) users.DELETE("/:id", handler.DeleteUser) users.POST("/:id/reset-password", handler.ResetPassword) users.POST("/ldap-import", handler.LdapImport) } // 流水线权限角色(管理员;Jenkins Item-role 正则风格) pipelineRoles := api.Group("/pipeline-roles", handler.AdminOnly()) { pipelineRoles.GET("", handler.ListPipelineRoles) pipelineRoles.POST("", handler.CreatePipelineRole) pipelineRoles.PUT("/:id", handler.UpdatePipelineRole) pipelineRoles.DELETE("/:id", handler.DeletePipelineRole) } api.POST("/ldap/users", handler.AdminOnly(), handler.LdapListUsers) api.POST("/settings/ldap/test", handler.AdminOnly(), handler.TestLDAP) api.GET("/docs", handler.GetDocs(docsFS)) api.GET("/summary", handler.GetSummary) api.GET("/settings", handler.AdminOnly(), handler.GetSettings) api.PUT("/settings", handler.AdminOnly(), handler.UpdateSettings) api.POST("/settings/test-llm", handler.AdminOnly(), handler.TestLLM) api.POST("/settings/test-lark", handler.AdminOnly(), handler.TestLark) api.POST("/ai/chat", handler.AiChat) api.POST("/ai/chat/stream", handler.AiChatStream) api.POST("/ai/release", handler.AiRelease(hub)) api.POST("/batch/parse", handler.BatchParse) // 流水线:查看 viewer / 编辑配置 owner / 触发构建 operator;新建与删除仅管理员 api.GET("/pipelines", handler.ListPipelines) api.POST("/pipelines", handler.AdminOnly(), handler.CreatePipeline) api.GET("/pipelines/:id", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelViewer)), handler.GetPipelineDetail) api.PUT("/pipelines/:id", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelOwner)), handler.UpdatePipeline) api.DELETE("/pipelines/:id", handler.AdminOnly(), handler.DeletePipeline) api.PUT("/pipelines/:id/nodes", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelOwner)), handler.SaveNodes) api.PUT("/pipelines/:id/edges", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelOwner)), handler.SaveEdges) api.POST("/pipelines/:id/runs", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelOperator)), handler.CreateRun(hub)) api.POST("/hooks/pipeline/:id", handler.InboundPipelineHook(hub)) // 单流水线成员:查看列表 viewer,增删 owner api.GET("/pipelines/:id/members", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelViewer)), handler.ListPipelineMembers) api.POST("/pipelines/:id/members", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelOwner)), handler.AddPipelineMember) api.DELETE("/pipelines/:id/members/:mid", handler.RequirePipelineAccess(handler.LevelRank(handler.LevelOwner)), handler.DeletePipelineMember) api.GET("/runs", handler.ListRuns) api.GET("/runs/:runId", handler.RequireRunAccess(handler.LevelRank(handler.LevelViewer)), handler.GetRun) api.GET("/runs/:runId/logs/stream", handler.RequireRunAccess(handler.LevelRank(handler.LevelViewer)), handler.StreamLogs(hub)) api.POST("/runs/:runId/retry", handler.RequireRunAccess(handler.LevelRank(handler.LevelOperator)), handler.RetryRun(hub)) // GitOps 部署(全局基础设施操作,未按流水线隔离)→ 管理员 gitops := api.Group("/gitops", handler.AdminOnly()) { gitops.GET("/apps", handler.ListGitOpsApps) gitops.GET("/argo/apps", handler.ArgoList) gitops.POST("/argo/sync", handler.ArgoSync) gitops.GET("/argo/:appName/tree", handler.ArgoResourceTree) gitops.GET("/argo/:appName/diff", handler.ArgoDiff) } // 插件市场 → 管理员 plugins := api.Group("/plugins", handler.AdminOnly()) { plugins.GET("", handler.ListPlugins) plugins.GET("/available", handler.AvailablePlugins) plugins.POST("/install", handler.InstallPlugin) plugins.POST("/:id/toggle", handler.TogglePlugin) plugins.DELETE("/:id", handler.UninstallPlugin) } // 资源(主机/集群/凭证/构建节点/工具/Webhook)与 Jenkins 集成 → 管理员 hosts := api.Group("/hosts", handler.AdminOnly()) { hosts.GET("", handler.ListHosts) hosts.POST("", handler.CreateHost) hosts.PUT("/:id", handler.UpdateHost) hosts.DELETE("/:id", handler.DeleteHost) hosts.POST("/:id/test", handler.TestHost) } clusters := api.Group("/clusters", handler.AdminOnly()) { clusters.GET("", handler.ListClusters) clusters.POST("", handler.CreateCluster) clusters.PUT("/:id", handler.UpdateCluster) clusters.DELETE("/:id", handler.DeleteCluster) clusters.POST("/:id/test", handler.TestCluster) } credentials := api.Group("/credentials", handler.AdminOnly()) { credentials.GET("", handler.ListCredentials) credentials.POST("", handler.CreateCredential) credentials.PUT("/:id", handler.UpdateCredential) credentials.DELETE("/:id", handler.DeleteCredential) } buildNodes := api.Group("/build-nodes", handler.AdminOnly()) { buildNodes.GET("", handler.ListBuildNodes) buildNodes.POST("", handler.CreateBuildNode) buildNodes.PUT("/:id", handler.UpdateBuildNode) buildNodes.DELETE("/:id", handler.DeleteBuildNode) buildNodes.POST("/:id/test", handler.TestBuildNode) buildNodes.POST("/:id/disk-test", handler.TestBuildNodeDisk) } tools := api.Group("/tools", handler.AdminOnly()) { tools.GET("", handler.ListTools) tools.PUT("/:id", handler.UpdateTool) tools.POST("/:id/check", handler.CheckTool) tools.POST("/:id/install", handler.InstallTool) } webhooks := api.Group("/webhooks", handler.AdminOnly()) { webhooks.GET("", handler.ListWebhooks) webhooks.POST("", handler.CreateWebhook) webhooks.PUT("/:id", handler.UpdateWebhook) webhooks.DELETE("/:id", handler.DeleteWebhook) webhooks.POST("/:id/test", handler.TestWebhook) } jenkins := api.Group("/jenkins-servers", handler.AdminOnly()) { jenkins.GET("", handler.ListJenkinsServers) jenkins.POST("", handler.CreateJenkinsServer) jenkins.PUT("/:id", handler.UpdateJenkinsServer) jenkins.DELETE("/:id", handler.DeleteJenkinsServer) jenkins.POST("/:id/test", handler.TestJenkinsServer) jenkins.GET("/:id/jobs", handler.ListJenkinsJobs) jenkins.POST("/:id/import", handler.ImportJenkinsJobs) } } distFS, _ := fs.Sub(staticFS, "static") r.NoRoute(func(c *gin.Context) { path := strings.TrimPrefix(c.Request.URL.Path, "/") if path != "" { if f, err := distFS.Open(path); err == nil { f.Close() c.FileFromFS("/"+path, http.FS(distFS)) return } } // SPA 路由回退:直接返回 index.html 内容 // (不能用 FileFromFS,http.FileServer 会把 /index.html 301 到 ./ 造成死循环) index, err := fs.ReadFile(distFS, "index.html") if err != nil { c.String(http.StatusNotFound, "前端资源未构建") return } c.Data(http.StatusOK, "text/html; charset=utf-8", index) }) port := os.Getenv("PORT") if port == "" { port = "9527" } if err := r.Run(":" + port); err != nil { panic("服务启动失败: " + err.Error()) } }