From 863a2986091cae5de5b315db2a25b262388cf364 Mon Sep 17 00:00:00 2001 From: engigu Date: Thu, 5 Mar 2026 22:22:46 +0800 Subject: [PATCH] feat: openapi supoort --- .github/workflows/release.yml | 3 + Makefile | 2 +- README.md | 1 + configs/config.example.ini | 8 +- docker/Dockerfile | 3 + internal/constant/constant.go | 19 +- internal/controllers/executor_controller.go | 33 +++ internal/controllers/settings_controller.go | 95 ++++-- internal/middleware/auth.go | 112 +++++++- internal/models/vo/sys_vo.go | 7 + internal/router/router.go | 120 ++++++-- internal/services/config_service.go | 21 -- main.go | 2 +- {docs => openapi_docs}/docs.go | 288 ++++++++++++++++--- {docs => openapi_docs}/swagger.json | 284 +++++++++++++++--- {docs => openapi_docs}/swagger.yaml | 192 ++++++++++--- web/src/api/index.ts | 4 + web/src/router/index.ts | 9 + web/src/views/error/NotFound.vue | 79 +++++ web/src/views/notify/components/ApiUsage.vue | 50 +++- web/src/views/settings/SiteSettings.vue | 157 +++++++++- web/vite.config.ts | 4 + 22 files changed, 1266 insertions(+), 227 deletions(-) rename {docs => openapi_docs}/docs.go (80%) rename {docs => openapi_docs}/swagger.json (80%) rename {docs => openapi_docs}/swagger.yaml (77%) create mode 100644 web/src/views/error/NotFound.vue diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c6cc4e..f703216 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,9 @@ jobs: - name: Build Web run: make build-web + - name: Generate Swagger + run: make swag + - name: Build Binaries run: | VERSION=${{ github.ref_name }} diff --git a/Makefile b/Makefile index 33da66a..5f9260f 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ deps: # Generate swagger documentation swag: - go run github.com/swaggo/swag/cmd/swag@latest init -g main.go -o ./docs + go run github.com/swaggo/swag/cmd/swag@latest init -g main.go -o ./openapi_docs # Docker build docker-build: diff --git a/README.md b/README.md index 459d1fc..037acca 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ### 最近更新 +**2026.03.05** - **API 文档重构** 重构 OpenAPI 认证体系,支持站点级 Token 配置与 Basic Auth 保护;新增设计感十足的全局 **404 页面**。 **2026.03.04** - 新增内置消息推送系统:全新原生支持企业微信、钉钉、飞书、Telegram、Bark、邮件等十余种主流渠道的推送,接入系统级事件通知自动捕获,告别原有必配外部推送服务的繁琐历史 **2026.02.13** - 重构任务执行引擎:深度集成 Mise 运行时管理,支持 Python, Node.js, Go, Rust, PHP 等几乎所有主流语言的动态安装与多版本切换,同步上线跨语言统一依赖管理系统 **2026.02.11** - 增强安全性:首次启动使用随机密码并打印在日志中,登录接口增加防暴力破解,文件系统操作增加路径穿越锁定 diff --git a/configs/config.example.ini b/configs/config.example.ini index c0880d9..083660e 100644 --- a/configs/config.example.ini +++ b/configs/config.example.ini @@ -30,10 +30,4 @@ table_prefix = baihu_ # 如果你手动设置了此项,它将覆盖数据库中的设置。 secret = -[swagger] -# 是否开启 Swagger API 文档 (开发测试建议开启,生产环境建议关闭或设置强密码) -enabled = true -# Swagger 文档 Basic Auth 账号 -user = admin -# Swagger 文档 Basic Auth 密码 -password = swagger_password + diff --git a/docker/Dockerfile b/docker/Dockerfile index 4d32a13..b4ae2ba 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -57,6 +57,9 @@ COPY . . # Copy frontend dist (needed for embedding) COPY --from=frontend-builder /app/web/dist ./internal/static/dist +# Generate Swagger +RUN go run github.com/swaggo/swag/cmd/swag@latest init -g main.go -o ./openapi_docs + # Build Go binary RUN VERSION_VAL=$(cat /build-info/version.txt) && \ BUILD_TIME_VAL=$(cat /build-info/build_time.txt) && \ diff --git a/internal/constant/constant.go b/internal/constant/constant.go index 2ba75a1..731d0c8 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -37,12 +37,13 @@ const ( SectionNotify = "notify" // Site Settings Key 常量 - KeyTitle = "title" - KeySubtitle = "subtitle" - KeyIcon = "icon" - KeyPageSize = "page_size" - KeyCookieDays = "cookie_days" - KeyApiToken = "api_token" + KeyTitle = "title" + KeySubtitle = "subtitle" + KeyIcon = "icon" + KeyPageSize = "page_size" + KeyCookieDays = "cookie_days" + KeyApiToken = "api_token" + KeyOpenapiToken = "openapi_token" // Security Settings Key 常量 KeySecret = "secret" @@ -65,9 +66,9 @@ const ( BindingTypeTask = "task" // 系统事件类型 - EventUserLogin = "user_login" - EventBruteForceLogin = "brute_force_login" - EventPasswordChanged = "password_changed" + EventUserLogin = "user_login" + EventBruteForceLogin = "brute_force_login" + EventPasswordChanged = "password_changed" // 任务事件类型 EventTaskSuccess = "task_success" diff --git a/internal/controllers/executor_controller.go b/internal/controllers/executor_controller.go index c73d32c..4137b98 100644 --- a/internal/controllers/executor_controller.go +++ b/internal/controllers/executor_controller.go @@ -18,6 +18,18 @@ func NewExecutorController(executorService *tasks.ExecutorService) *ExecutorCont return &ExecutorController{executorService: executorService} } +// ExecuteTask 运行任务 +// @Summary 运行任务 +// @Description 立即执行指定的任务 +// @Tags 任务 +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param id path string true "任务ID" +// @Param body body object false "执行参数 (envs: 环境变量字典)" +// @Success 200 {object} utils.Response{data=vo.ExecutionResultVO} +// @Failure 400 {object} utils.Response +// @Router /execute/task/{id} [post] func (ec *ExecutorController) ExecuteTask(c *gin.Context) { id := c.Param("id") if id == "" { @@ -42,6 +54,17 @@ func (ec *ExecutorController) ExecuteTask(c *gin.Context) { utils.Success(c, vo.ToExecutionResultVO(result)) } +// ExecuteCommand 执行命令 +// @Summary 执行命令 +// @Description 临时执行单次命令 +// @Tags 任务执行 +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body object true "命令内容 (需包含 command 字段)" +// @Success 200 {object} utils.Response{data=vo.ExecutionResultVO} +// @Failure 400 {object} utils.Response +// @Router /execute/command [post] func (ec *ExecutorController) ExecuteCommand(c *gin.Context) { var req struct { Command string `json:"command" binding:"required"` @@ -56,6 +79,16 @@ func (ec *ExecutorController) ExecuteCommand(c *gin.Context) { utils.Success(c, vo.ToExecutionResultVO(result)) } +// GetLastResults 获取最新执行结果 +// @Summary 获取最新执行结果 +// @Description 获取最新任务或命令执行的结果列表 +// @Tags 任务执行 +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param count query int false "数量 (默认 10)" +// @Success 200 {object} utils.Response{data=[]vo.ExecutionResultVO} +// @Router /execute/results [get] func (ec *ExecutorController) GetLastResults(c *gin.Context) { count := 10 if c.Query("count") != "" { diff --git a/internal/controllers/settings_controller.go b/internal/controllers/settings_controller.go index 3027e00..4688191 100644 --- a/internal/controllers/settings_controller.go +++ b/internal/controllers/settings_controller.go @@ -88,16 +88,30 @@ func (sc *SettingsController) ChangePassword(c *gin.Context) { // GetSiteSettings 获取站点设置 func (sc *SettingsController) GetSiteSettings(c *gin.Context) { settings := sc.settingsService.GetSection(constant.SectionSite) - + // 解析 JSON 格式的 API Token if tokenJson, ok := settings[constant.KeyApiToken]; ok && tokenJson != "" { - var tokenData map[string]string - if err := json.Unmarshal([]byte(tokenJson), &tokenData); err == nil { - settings["api_token"] = tokenData["token"] - settings["api_token_expire"] = tokenData["expire_at"] + var tokenConfig vo.TokenConfig + if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err == nil { + settings["api_token"] = tokenConfig.Token + settings["api_token_expire"] = tokenConfig.ExpireAt } } - + + // 解析 JSON 格式的 OpenAPI Token + if tokenJson, ok := settings[constant.KeyOpenapiToken]; ok && tokenJson != "" { + var tokenConfig vo.TokenConfig + if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err == nil { + settings["openapi_token"] = tokenConfig.Token + settings["openapi_token_expire"] = tokenConfig.ExpireAt + if tokenConfig.Enabled { + settings["openapi_enabled"] = "true" + } else { + settings["openapi_enabled"] = "false" + } + } + } + utils.Success(c, settings) } @@ -116,13 +130,16 @@ func (sc *SettingsController) GetPublicSiteSettings(c *gin.Context) { // UpdateSiteSettings 更新站点设置 func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) { var req struct { - Title string `json:"title"` - Subtitle string `json:"subtitle"` - Icon string `json:"icon"` - PageSize string `json:"page_size"` - CookieDays string `json:"cookie_days"` - ApiToken string `json:"api_token"` - ApiTokenExpire string `json:"api_token_expire"` + Title string `json:"title"` + Subtitle string `json:"subtitle"` + Icon string `json:"icon"` + PageSize string `json:"page_size"` + CookieDays string `json:"cookie_days"` + ApiToken string `json:"api_token"` + ApiTokenExpire string `json:"api_token_expire"` + OpenapiEnabled bool `json:"openapi_enabled"` + OpenapiToken string `json:"openapi_token"` + OpenapiTokenExpire string `json:"openapi_token_expire"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -132,22 +149,35 @@ func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) { apiTokenJson := "" if req.ApiToken != "" || req.ApiTokenExpire != "" { - tokenData := map[string]string{ - "token": req.ApiToken, - "expire_at": req.ApiTokenExpire, + tokenConfig := vo.TokenConfig{ + Token: req.ApiToken, + ExpireAt: req.ApiTokenExpire, } - if b, err := json.Marshal(tokenData); err == nil { + if b, err := json.Marshal(tokenConfig); err == nil { apiTokenJson = string(b) } } + openapiTokenJson := "" + if req.OpenapiToken != "" || req.OpenapiTokenExpire != "" || req.OpenapiEnabled { + tokenConfig := vo.TokenConfig{ + Enabled: req.OpenapiEnabled, + Token: req.OpenapiToken, + ExpireAt: req.OpenapiTokenExpire, + } + if b, err := json.Marshal(tokenConfig); err == nil { + openapiTokenJson = string(b) + } + } + values := map[string]string{ - constant.KeyTitle: req.Title, - constant.KeySubtitle: req.Subtitle, - constant.KeyIcon: req.Icon, - constant.KeyPageSize: req.PageSize, - constant.KeyCookieDays: req.CookieDays, - constant.KeyApiToken: apiTokenJson, + constant.KeyTitle: req.Title, + constant.KeySubtitle: req.Subtitle, + constant.KeyIcon: req.Icon, + constant.KeyPageSize: req.PageSize, + constant.KeyCookieDays: req.CookieDays, + constant.KeyApiToken: apiTokenJson, + constant.KeyOpenapiToken: openapiTokenJson, } if err := sc.settingsService.SetSection(constant.SectionSite, values); err != nil { @@ -165,6 +195,13 @@ func (sc *SettingsController) GenerateApiToken(c *gin.Context) { }) } +// GenerateOpenapiToken 随机生成OpenAPI Token +func (sc *SettingsController) GenerateOpenapiToken(c *gin.Context) { + utils.Success(c, gin.H{ + "token": strings.ToLower(utils.RandomString(32)), + }) +} + // GetSchedulerSettings 获取调度设置 func (sc *SettingsController) GetSchedulerSettings(c *gin.Context) { settings := sc.settingsService.GetSection(constant.SectionScheduler) @@ -382,12 +419,12 @@ func (sc *SettingsController) RestoreBackup(c *gin.Context) { func (sc *SettingsController) GetSetting(c *gin.Context) { section := c.Param("section") key := c.Param("key") - + if section == "" || key == "" { utils.BadRequest(c, "参数错误") return } - + value := sc.settingsService.Get(section, key) utils.Success(c, value) } @@ -396,20 +433,20 @@ func (sc *SettingsController) GetSetting(c *gin.Context) { func (sc *SettingsController) GenerateSettingToken(c *gin.Context) { section := c.Param("section") key := c.Param("key") - + if section == "" || key == "" { utils.BadRequest(c, "参数错误") return } - + // 生成32位随机token token := strings.ToLower(utils.RandomString(32)) - + // 保存到数据库 if err := sc.settingsService.Set(section, key, token); err != nil { utils.ServerError(c, "保存失败") return } - + utils.Success(c, token) } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 5e2f985..ac0ae01 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -8,6 +8,7 @@ import ( "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/models" + "github.com/engigu/baihu-panel/internal/models/vo" "github.com/engigu/baihu-panel/internal/services" "github.com/engigu/baihu-panel/internal/utils" @@ -23,6 +24,11 @@ func AuthRequired() gin.HandlerFunc { return } + // 校验 OpenAPI Token + if checkOpenapiToken(c, settingsSvc) { + return + } + token, err := c.Cookie(constant.CookieName) if err != nil || token == "" { utils.Unauthorized(c, "请先登录") @@ -69,20 +75,19 @@ func checkApiToken(c *gin.Context, settingsSvc *services.SettingsService) bool { return false } - var tokenData map[string]string - if err := json.Unmarshal([]byte(tokenJson), &tokenData); err != nil { + var tokenConfig vo.TokenConfig + if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err != nil { return false } - expectedToken, ok := tokenData["token"] - if !ok || expectedToken == "" || apiToken != expectedToken { + if tokenConfig.Token == "" || apiToken != tokenConfig.Token { return false } // 检查过期时间 - if expireStr, ok := tokenData["expire_at"]; ok && expireStr != "" { + if tokenConfig.ExpireAt != "" { // 前端传来的时间格式是 YYYY-MM-DD,使用 2006-01-02 解析 - expireDate, err := time.Parse("2006-01-02", expireStr) + expireDate, err := time.Parse("2006-01-02", tokenConfig.ExpireAt) if err == nil { // 将过期时间设为当天的 23:59:59 expireDate = expireDate.Add(23*time.Hour + 59*time.Minute + 59*time.Second) @@ -99,7 +104,61 @@ func checkApiToken(c *gin.Context, settingsSvc *services.SettingsService) bool { c.Abort() return true // 返回 true 表示中间件已处理并截断了请求 } - + + c.Set("userID", adminUser.ID) + c.Set("username", adminUser.Username) + c.Next() + return true +} + +// checkOpenapiToken 校验 OpenAPI Token +// 返回 true 表示校验通过并已放行请求 +func checkOpenapiToken(c *gin.Context, settingsSvc *services.SettingsService) bool { + authHeader := c.GetHeader("Authorization") + if authHeader == "" || len(authHeader) < 8 || authHeader[:7] != "Bearer " { + return false + } + openapiToken := authHeader[7:] + + siteConfig := settingsSvc.GetSection(constant.SectionSite) + tokenJson, ok := siteConfig[constant.KeyOpenapiToken] + if !ok || tokenJson == "" { + return false + } + + var tokenConfig vo.TokenConfig + if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err != nil { + return false + } + + // 校验开启状态 + if !tokenConfig.Enabled { + return false + } + + if tokenConfig.Token == "" || openapiToken != tokenConfig.Token { + return false + } + + // 检查过期时间 + if tokenConfig.ExpireAt != "" { + expireDate, err := time.Parse("2006-01-02", tokenConfig.ExpireAt) + if err == nil { + expireDate = expireDate.Add(23*time.Hour + 59*time.Minute + 59*time.Second) + if time.Now().After(expireDate) { + return false + } + } + } + + // 模拟 Admin 角色 + var adminUser models.User + if err := database.DB.Where("role = ?", "admin").First(&adminUser).Error; err != nil { + utils.Unauthorized(c, "未找到管理员账户,OpenAPI Token 校验失败") + c.Abort() + return true + } + c.Set("userID", adminUser.ID) c.Set("username", adminUser.Username) c.Next() @@ -119,22 +178,47 @@ func ClearAuthCookie(c *gin.Context) { // SwaggerAuth Swagger 认证中间件 (Basic Auth) func SwaggerAuth() gin.HandlerFunc { + settingsSvc := services.NewSettingsService() return func(c *gin.Context) { - cfg := services.GetConfig() - if !cfg.Swagger.Enabled { - c.Status(404) + siteConfig := settingsSvc.GetSection(constant.SectionSite) + tokenJson, ok := siteConfig[constant.KeyOpenapiToken] + if !ok || tokenJson == "" { + c.Status(http.StatusNotFound) c.Abort() return } - user, password, hasAuth := c.Request.BasicAuth() - if hasAuth && user == cfg.Swagger.User && password == cfg.Swagger.Password { + var tokenConfig vo.TokenConfig + if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err != nil { + c.Status(http.StatusNotFound) + c.Abort() + return + } + + // 必须开启鉴权开关 + if !tokenConfig.Enabled { + c.Status(http.StatusNotFound) + c.Abort() + return + } + + _, password, hasAuth := c.Request.BasicAuth() + // 允许使用任意用户名,但密码必须匹配 OpenAPI Token + if hasAuth && password == tokenConfig.Token && tokenConfig.Token != "" { c.Next() return } - c.Header("WWW-Authenticate", `Basic realm="restricted"`) - c.Status(http.StatusUnauthorized) + // 认证失败,提示输入密码 (如果未提供认证) + if !hasAuth { + c.Header("WWW-Authenticate", `Basic realm="OpenAPI Access Token (Any username)"`) + c.Status(http.StatusUnauthorized) + c.Abort() + return + } + + // 认证失败 (密码错误),返回 404 隐藏路由 + c.Status(http.StatusNotFound) c.Abort() } } diff --git a/internal/models/vo/sys_vo.go b/internal/models/vo/sys_vo.go index eb65421..36024f2 100644 --- a/internal/models/vo/sys_vo.go +++ b/internal/models/vo/sys_vo.go @@ -124,3 +124,10 @@ func ToLoginLogVOListFromModels(logs []models.LoginLog) []*LoginLogVO { } return vos } + +// TokenConfig Token 配置结构体 +type TokenConfig struct { + Enabled bool `json:"enabled"` + Token string `json:"token"` + ExpireAt string `json:"expire_at"` +} diff --git a/internal/router/router.go b/internal/router/router.go index a946e28..b29b9fa 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -1,12 +1,15 @@ package router import ( + "encoding/json" "io/fs" "net/http" "strings" + "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/controllers" "github.com/engigu/baihu-panel/internal/middleware" + "github.com/engigu/baihu-panel/internal/models/vo" "github.com/engigu/baihu-panel/internal/services" "github.com/engigu/baihu-panel/internal/static" @@ -85,8 +88,79 @@ func Setup(c *Controllers) *gin.Engine { }) } - // Swagger documentation (带 Basic Auth 认证) - root.GET("/swagger/*any", middleware.SwaggerAuth(), ginSwagger.WrapHandler(swaggerFiles.Handler)) + // OpenAPI documentation using Scalar UI (带 Basic Auth 认证) + router.GET("/openapi/*any", func(c *gin.Context) { + settingsSvc := services.NewSettingsService() + siteConfig := settingsSvc.GetSection(constant.SectionSite) + tokenJson := siteConfig[constant.KeyOpenapiToken] + + enabled := false + if tokenJson != "" { + var tokenConfig vo.TokenConfig + if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err == nil { + enabled = tokenConfig.Enabled + } + } + + // 如果未开启文档,直接返回 404 SPA 页面 + if !enabled { + serveSPA(c, urlPrefix, 404) + return + } + + // 执行认证 + middleware.SwaggerAuth()(c) + if c.IsAborted() { + // 如果认证失败(且被中间件置为 404,如密码错误且我们想要隐藏它) + if c.Writer.Status() == 404 { + serveSPA(c, urlPrefix, 404) + } + return + } + + // 获取内部路径(移除开头的斜杠) + path := strings.TrimPrefix(c.Param("any"), "/") + + // 1. 根路径或空路径 -> 重定向到 index.html + if path == "" || path == "/" { + c.Redirect(http.StatusMovedPermanently, urlPrefix+"/openapi/index.html") + return + } + + // 2. 提供 Scalar 渲染的 HTML 页面 + if path == "index.html" { + scalarHTML := ` + + + Baihu Panel API Reference + + + + + + + + +` + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, scalarHTML) + c.Abort() + return + } + + // 3. 提供给 Scalar/Swagger 使用的 doc.json 内容 + if path == "doc.json" { + // 这里借助 ginSwagger 仅生成 doc.json 内容 + h := ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL(urlPrefix+"/openapi/doc.json")) + h(c) + return + } + + // 4. 其余路径一律返回 SPA 的 404 + serveSPA(c, urlPrefix, 404) + }) // API 路由组 api := root.Group("/api/v1") @@ -199,6 +273,7 @@ func Setup(c *Controllers) *gin.Engine { settings.GET("/site", c.Settings.GetSiteSettings) settings.PUT("/site", c.Settings.UpdateSiteSettings) settings.POST("/site/api-token/generate", c.Settings.GenerateApiToken) + settings.POST("/site/openapi-token/generate", c.Settings.GenerateOpenapiToken) settings.GET("/paths", c.Settings.GetPaths) settings.GET("/scheduler", c.Settings.GetSchedulerSettings) settings.PUT("/scheduler", c.Settings.UpdateSchedulerSettings) @@ -314,21 +389,34 @@ func Setup(c *Controllers) *gin.Engine { return } - data, err := static.ReadFile("index.html") - if err != nil { - ctx.Status(404) - return - } - - html := string(data) - - // 注入配置变量供前端使用(API 调用和路由) - configScript := `` - html = strings.Replace(html, "", configScript+"", 1) - - ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate") - ctx.Data(200, "text/html; charset=utf-8", []byte(html)) + serveSPA(ctx, urlPrefix, 200) }) return router } + +// serveSPA 注入配置并返回 index.html 给前端渲染 +func serveSPA(ctx *gin.Context, urlPrefix string, status int) { + data, err := static.ReadFile("index.html") + if err != nil { + // 如果读不到 index.html (如 dev 模式未 build),返回基础 HTML 触发前端路由 + fallback := `404 Not Found + +

Not Found. Redirecting to home...

+ ` + ctx.Header("Content-Type", "text/html; charset=utf-8") + ctx.Data(status, "text/html", []byte(fallback)) + ctx.Abort() + return + } + + html := string(data) + + // 注入配置变量供前端使用(API 调用和路由) + configScript := `` + html = strings.Replace(html, "", configScript+"", 1) + + ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate") + ctx.Data(status, "text/html; charset=utf-8", []byte(html)) + ctx.Abort() +} diff --git a/internal/services/config_service.go b/internal/services/config_service.go index f94f321..76b96a5 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -31,17 +31,10 @@ type SecurityConfig struct { Secret string `ini:"secret"` } -type SwaggerConfig struct { - Enabled bool `ini:"enabled"` - User string `ini:"user"` - Password string `ini:"password"` -} - type AppConfig struct { Server ServerConfig `ini:"server"` Database DatabaseConfig `ini:"database"` Security SecurityConfig `ini:"security"` - Swagger SwaggerConfig `ini:"swagger"` } var Config *AppConfig @@ -82,11 +75,6 @@ func LoadConfig(path string) (*AppConfig, error) { Security: SecurityConfig{ Secret: "", }, - Swagger: SwaggerConfig{ - Enabled: false, - User: "admin", - Password: "swagger_password", - }, } // 检查配置文件是否存在 @@ -154,15 +142,6 @@ func applyEnvOverrides() { // Security getEnvStr("BH_SECRET", &Config.Security.Secret) - // Swagger - vSwaggerEnabled := os.Getenv("BH_SWAGGER_ENABLED") - if vSwaggerEnabled == "true" || vSwaggerEnabled == "1" { - Config.Swagger.Enabled = true - } else if vSwaggerEnabled == "false" || vSwaggerEnabled == "0" { - Config.Swagger.Enabled = false - } - getEnvStr("BH_SWAGGER_USER", &Config.Swagger.User) - getEnvStr("BH_SWAGGER_PASSWORD", &Config.Swagger.Password) } func GetConfig() *AppConfig { diff --git a/main.go b/main.go index de90b4a..d50dc61 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "github.com/engigu/baihu-panel/cmd" "github.com/engigu/baihu-panel/internal/bootstrap" "github.com/engigu/baihu-panel/internal/constant" - _ "github.com/engigu/baihu-panel/docs" // This will be generated by swag init + _ "github.com/engigu/baihu-panel/openapi_docs" // This will be generated by swag init ) // @title Baihu Panel API diff --git a/docs/docs.go b/openapi_docs/docs.go similarity index 80% rename from docs/docs.go rename to openapi_docs/docs.go index 702c651..cb73010 100644 --- a/docs/docs.go +++ b/openapi_docs/docs.go @@ -1,5 +1,5 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs +// Package openapi_docs Code generated by swaggo/swag. DO NOT EDIT +package openapi_docs import "github.com/swaggo/swag" @@ -68,7 +68,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -132,7 +132,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -148,7 +148,7 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -178,7 +178,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -230,7 +230,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -246,7 +246,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -292,7 +292,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -308,7 +308,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -349,7 +349,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "409": { @@ -357,7 +357,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -409,7 +409,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -428,6 +428,177 @@ const docTemplate = `{ } } }, + "/execute/command": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "临时执行单次命令", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "任务执行" + ], + "summary": "执行命令", + "parameters": [ + { + "description": "命令内容 (需包含 command 字段)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.ExecutionResultVO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.Response" + } + } + } + } + }, + "/execute/results": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取最新任务或命令执行的结果列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "任务执行" + ], + "summary": "获取最新执行结果", + "parameters": [ + { + "type": "integer", + "description": "数量 (默认 10)", + "name": "count", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.ExecutionResultVO" + } + } + } + } + ] + } + } + } + } + }, + "/execute/task/{id}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "立即执行指定的任务", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "任务" + ], + "summary": "运行任务", + "parameters": [ + { + "type": "string", + "description": "任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "执行参数 (envs: 环境变量字典)", + "name": "body", + "in": "body", + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.ExecutionResultVO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.Response" + } + } + } + } + }, "/logs": { "get": { "security": [ @@ -484,7 +655,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -547,13 +718,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -592,7 +763,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -608,7 +779,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -643,13 +814,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -717,7 +888,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -779,13 +950,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -824,7 +995,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -840,7 +1011,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -886,7 +1057,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -902,7 +1073,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -937,13 +1108,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -951,18 +1122,6 @@ const docTemplate = `{ } }, "definitions": { - "github_com_engigu_baihu-panel_internal_utils.Response": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - } - }, "utils.PaginationData": { "type": "object", "properties": { @@ -978,6 +1137,18 @@ const docTemplate = `{ } } }, + "utils.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, "vo.EnvVO": { "type": "object", "properties": { @@ -1004,6 +1175,41 @@ const docTemplate = `{ } } }, + "vo.ExecutionResultVO": { + "type": "object", + "properties": { + "duration": { + "type": "integer" + }, + "end_time": { + "type": "string" + }, + "error": { + "type": "string" + }, + "exit_code": { + "type": "integer" + }, + "log_id": { + "type": "string" + }, + "output": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "status": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "task_id": { + "type": "string" + } + } + }, "vo.TaskLogVO": { "type": "object", "properties": { diff --git a/docs/swagger.json b/openapi_docs/swagger.json similarity index 80% rename from docs/swagger.json rename to openapi_docs/swagger.json index 029ef02..9e1d328 100644 --- a/docs/swagger.json +++ b/openapi_docs/swagger.json @@ -62,7 +62,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -126,7 +126,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -142,7 +142,7 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -172,7 +172,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -224,7 +224,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -240,7 +240,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -286,7 +286,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -302,7 +302,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -343,7 +343,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "409": { @@ -351,7 +351,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -403,7 +403,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -422,6 +422,177 @@ } } }, + "/execute/command": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "临时执行单次命令", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "任务执行" + ], + "summary": "执行命令", + "parameters": [ + { + "description": "命令内容 (需包含 command 字段)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.ExecutionResultVO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.Response" + } + } + } + } + }, + "/execute/results": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取最新任务或命令执行的结果列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "任务执行" + ], + "summary": "获取最新执行结果", + "parameters": [ + { + "type": "integer", + "description": "数量 (默认 10)", + "name": "count", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/vo.ExecutionResultVO" + } + } + } + } + ] + } + } + } + } + }, + "/execute/task/{id}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "立即执行指定的任务", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "任务" + ], + "summary": "运行任务", + "parameters": [ + { + "type": "string", + "description": "任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "执行参数 (envs: 环境变量字典)", + "name": "body", + "in": "body", + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/vo.ExecutionResultVO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/utils.Response" + } + } + } + } + }, "/logs": { "get": { "security": [ @@ -478,7 +649,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -541,13 +712,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -586,7 +757,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -602,7 +773,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -637,13 +808,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -711,7 +882,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -773,13 +944,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -818,7 +989,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -834,7 +1005,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -880,7 +1051,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" }, { "type": "object", @@ -896,7 +1067,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -931,13 +1102,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response" + "$ref": "#/definitions/utils.Response" } } } @@ -945,18 +1116,6 @@ } }, "definitions": { - "github_com_engigu_baihu-panel_internal_utils.Response": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - } - }, "utils.PaginationData": { "type": "object", "properties": { @@ -972,6 +1131,18 @@ } } }, + "utils.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, "vo.EnvVO": { "type": "object", "properties": { @@ -998,6 +1169,41 @@ } } }, + "vo.ExecutionResultVO": { + "type": "object", + "properties": { + "duration": { + "type": "integer" + }, + "end_time": { + "type": "string" + }, + "error": { + "type": "string" + }, + "exit_code": { + "type": "integer" + }, + "log_id": { + "type": "string" + }, + "output": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "status": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "task_id": { + "type": "string" + } + } + }, "vo.TaskLogVO": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/openapi_docs/swagger.yaml similarity index 77% rename from docs/swagger.yaml rename to openapi_docs/swagger.yaml index cebc7f2..ffc6ab6 100644 --- a/docs/swagger.yaml +++ b/openapi_docs/swagger.yaml @@ -1,13 +1,5 @@ basePath: /api/v1 definitions: - github_com_engigu_baihu-panel_internal_utils.Response: - properties: - code: - type: integer - data: {} - msg: - type: string - type: object utils.PaginationData: properties: list: {} @@ -18,6 +10,14 @@ definitions: total: type: integer type: object + utils.Response: + properties: + code: + type: integer + data: {} + msg: + type: string + type: object vo.EnvVO: properties: created_at: @@ -35,6 +35,29 @@ definitions: value: type: string type: object + vo.ExecutionResultVO: + properties: + duration: + type: integer + end_time: + type: string + error: + type: string + exit_code: + type: integer + log_id: + type: string + output: + type: string + start_time: + type: string + status: + type: string + success: + type: boolean + task_id: + type: string + type: object vo.TaskLogVO: properties: agent_id: @@ -156,7 +179,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: allOf: @@ -191,7 +214,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: $ref: '#/definitions/vo.EnvVO' @@ -199,7 +222,7 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 创建环境变量 @@ -226,12 +249,12 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' "409": description: 引用冲突 schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: items: @@ -260,7 +283,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: $ref: '#/definitions/vo.EnvVO' @@ -268,7 +291,7 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 获取环境变量详情 @@ -297,7 +320,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: $ref: '#/definitions/vo.EnvVO' @@ -305,7 +328,7 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 更新环境变量 @@ -329,7 +352,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: items: @@ -353,7 +376,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: items: @@ -365,6 +388,105 @@ paths: summary: 获取所有环境变量 tags: - 环境变量 + /execute/command: + post: + consumes: + - application/json + description: 临时执行单次命令 + parameters: + - description: 命令内容 (需包含 command 字段) + in: body + name: body + required: true + schema: + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/utils.Response' + - properties: + data: + $ref: '#/definitions/vo.ExecutionResultVO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.Response' + security: + - ApiKeyAuth: [] + summary: 执行命令 + tags: + - 任务执行 + /execute/results: + get: + consumes: + - application/json + description: 获取最新任务或命令执行的结果列表 + parameters: + - description: 数量 (默认 10) + in: query + name: count + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/utils.Response' + - properties: + data: + items: + $ref: '#/definitions/vo.ExecutionResultVO' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: 获取最新执行结果 + tags: + - 任务执行 + /execute/task/{id}: + post: + consumes: + - application/json + description: 立即执行指定的任务 + parameters: + - description: 任务ID + in: path + name: id + required: true + type: string + - description: '执行参数 (envs: 环境变量字典)' + in: body + name: body + schema: + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/utils.Response' + - properties: + data: + $ref: '#/definitions/vo.ExecutionResultVO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/utils.Response' + security: + - ApiKeyAuth: [] + summary: 运行任务 + tags: + - 任务 /logs: get: consumes: @@ -398,7 +520,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: allOf: @@ -432,11 +554,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' "500": description: Internal Server Error schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 删除日志 @@ -459,7 +581,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: $ref: '#/definitions/vo.TaskLogVO' @@ -467,7 +589,7 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 获取日志详情 @@ -490,11 +612,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' "500": description: Internal Server Error schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 清空日志 @@ -537,7 +659,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: allOf: @@ -571,11 +693,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' "404": description: Not Found schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 删除任务 @@ -598,7 +720,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: $ref: '#/definitions/vo.TaskVO' @@ -606,7 +728,7 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 获取任务详情 @@ -635,7 +757,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + - $ref: '#/definitions/utils.Response' - properties: data: $ref: '#/definitions/vo.TaskVO' @@ -643,7 +765,7 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 更新任务 @@ -666,11 +788,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' "400": description: Bad Request schema: - $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response' + $ref: '#/definitions/utils.Response' security: - ApiKeyAuth: [] summary: 停止任务 diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 56a0e76..5b1fa50 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -133,6 +133,7 @@ export const api = { updateSite: (data: SiteSettings) => request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }), generateApiToken: () => request<{ token: string }>('/settings/site/api-token/generate', { method: 'POST' }), + generateOpenapiToken: () => request<{ token: string }>('/settings/site/openapi-token/generate', { method: 'POST' }), getScheduler: () => request('/settings/scheduler'), updateScheduler: (data: SchedulerSettings) => request('/settings/scheduler', { method: 'PUT', body: JSON.stringify(data) }), @@ -431,6 +432,9 @@ export interface SiteSettings { icon: string page_size: string cookie_days: string + openapi_enabled?: boolean + openapi_token?: string + openapi_token_expire?: string api_token?: string api_token_expire?: string } diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 08211bb..d41ae04 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -50,6 +50,15 @@ const router = createRouter({ { path: 'notify', name: 'notify', component: () => import('@/views/notify/Notify.vue') }, { path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') } ] + }, + { + path: '/404', + name: '404', + component: () => import('@/views/error/NotFound.vue') + }, + { + path: '/:pathMatch(.*)*', + redirect: '/404' } ] }) diff --git a/web/src/views/error/NotFound.vue b/web/src/views/error/NotFound.vue new file mode 100644 index 0000000..08ac494 --- /dev/null +++ b/web/src/views/error/NotFound.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/web/src/views/notify/components/ApiUsage.vue b/web/src/views/notify/components/ApiUsage.vue index f3b6c2a..4c82d64 100644 --- a/web/src/views/notify/components/ApiUsage.vue +++ b/web/src/views/notify/components/ApiUsage.vue @@ -3,7 +3,17 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { Copy, Terminal, Key, FileJson, RefreshCw, Check, Hash, Info } from 'lucide-vue-next' +import { Copy, Terminal, Key, FileJson, RefreshCw, Check, Hash, Info, AlertTriangle } from 'lucide-vue-next' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import type { NotifyChannel, ChannelType } from '@/api' import { ref, computed } from 'vue' import { toast } from 'vue-sonner' @@ -22,6 +32,20 @@ const emit = defineEmits<{ const copiedBlock = ref(null) const host = ref(window.location.host) +const showConfirmDialog = ref(false) + +function onGenerateClick() { + if (props.apiToken) { + showConfirmDialog.value = true + } else { + emit('generateToken') + } +} + +function handleConfirmGenerate() { + showConfirmDialog.value = false + emit('generateToken') +} function copyToClipboard(text: string, blockId: string) { navigator.clipboard.writeText(text).then(() => { @@ -74,8 +98,7 @@ const shellExample = computed(() => `curl -s -X POST "http://${host.value}/api/v - @@ -191,7 +214,7 @@ const shellExample = computed(() => `curl -s -X POST "http://${host.value}/api/v

# 使用 CURL 调用推送接口

curl -s -X POST "http://{{ host - }}/api/v1/notify/send" \

+ }}/api/v1/notify/send" \

-H "Content-Type: application/json" \

-H "notify-token: {{ apiToken || @@ -227,6 +250,25 @@ const shellExample = computed(() => `curl -s -X POST "http://${host.value}/api/v

+ + + + + + + + 确认重新生成 Token? + + + 此操作将立刻覆盖当前 Token,旧的 Token 将会永久失效。确认要继续吗? + + + + 取消 + 确认重新生成 + + + diff --git a/web/src/views/settings/SiteSettings.vue b/web/src/views/settings/SiteSettings.vue index 7ae7a69..44f4122 100644 --- a/web/src/views/settings/SiteSettings.vue +++ b/web/src/views/settings/SiteSettings.vue @@ -7,20 +7,38 @@ import { api, type SiteSettings } from '@/api' import { toast } from 'vue-sonner' import { useSiteSettings } from '@/composables/useSiteSettings' import { Badge } from '@/components/ui/badge' -import { RefreshCw, Copy } from 'lucide-vue-next' +import { Switch } from '@/components/ui/switch' +import { RefreshCw, Copy, AlertTriangle, ExternalLink } from 'lucide-vue-next' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' const { refreshSettings } = useSiteSettings() +const baseUrl = (window as any).__BASE_URL__ || '' + const form = ref({ title: '', subtitle: '', icon: '', page_size: '10', cookie_days: '7', + openapi_enabled: false, + openapi_token: '', + openapi_token_expire: '', api_token: '', api_token_expire: '' }) const loading = ref(false) +const showOpenapiConfirmDialog = ref(false) +const showApiConfirmDialog = ref(false) const iconPreview = computed(() => { if (!form.value.icon) return '' @@ -34,8 +52,11 @@ const iconPreview = computed(() => { async function loadSettings() { try { const res = await api.settings.getSite() - form.value = res - } catch {} + form.value = { + ...res, + openapi_enabled: res.openapi_enabled === true || (res as any).openapi_enabled === 'true' + } + } catch { } } async function saveSettings() { @@ -55,11 +76,27 @@ async function saveSettings() { } } +async function generateOpenapiToken() { + try { + const res = await api.settings.generateOpenapiToken() + form.value.openapi_token = res.token + + // 如果没有设置过期时间,默认给一年后 + if (!form.value.openapi_token_expire) { + const d = new Date() + d.setFullYear(d.getFullYear() + 1) + form.value.openapi_token_expire = d.toISOString().split('T')[0] + } + } catch { + toast.error('生成 Token 失败') + } +} + async function generateToken() { try { const res = await api.settings.generateApiToken() form.value.api_token = res.token - + // 如果没有设置过期时间,默认给一年后 if (!form.value.api_token_expire) { const d = new Date() @@ -71,6 +108,16 @@ async function generateToken() { } } +async function copyOpenapiToken() { + if (!form.value.openapi_token) return + try { + await navigator.clipboard.writeText(form.value.openapi_token) + toast.success('Token 已复制到剪贴板') + } catch { + toast.error('复制失败,请手动复制') + } +} + async function copyToken() { if (!form.value.api_token) return try { @@ -98,7 +145,9 @@ onMounted(loadSettings)
-
+
@@ -114,19 +163,69 @@ onMounted(loadSettings)
- + +
+
+
+

OpenAPI Token

+ 推荐方式 +
+
+ + 查看接口文档 + + +
+ + +
+
+
+

开启全局 OpenAPI 直接访问能力,配置后可通过请求头 Authorization: Bearer <在此生成的Token> + 无需登录直接调用系统的所有接口,请妥善保管并设置合理的有效期。

+ +
+ +
+ + + +
+
+ +
+ +
+ +
超过此日期后该 Token 将失效,置空代表该特性完全关闭。
+
+
+
+

API Token

- 实验特性,可能变更 + + 实验特性,可能变更,将会在后期下线
-

开启全局 API 直接访问能力,配置后可通过请求头 X-API-Token: <在此生成的Token> 无需登录直接调用系统的所有接口,请妥善保管并设置合理的有效期。

- +

开启全局 API 直接访问能力,配置后可通过请求头 X-API-Token: <在此生成的Token> + 无需登录直接调用系统的所有接口,请妥善保管并设置合理的有效期。

+
-
+ + + + + + + + 确认重新生成 Token? + + + 此操作将立刻覆盖当前配置框内的 OpenAPI Token,原有的 Token 在点击【保存设置】后将会永久失效,导致所有使用旧 Token 的外部系统无法访问。确认要继续吗? + + + + 取消 + 重新生成 + + + + + + + + + + + 确认重新生成 Token? + + + 此操作将立刻覆盖当前配置框内的旧版 API Token,原有的 Token 在点击【保存设置】后将会永久失效。建议逐步迁移到 OpenAPI Token 后直接将此特性置空关闭。确认要继续吗? + + + + 取消 + 重新生成 + + +
diff --git a/web/vite.config.ts b/web/vite.config.ts index 02a7f91..c080d5f 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -16,6 +16,10 @@ export default defineConfig({ target: 'http://localhost:8052', changeOrigin: true, ws: true + }, + '/openapi': { + target: 'http://localhost:8052', + changeOrigin: true } } },