feat: openapi supoort
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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") != "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+98
-14
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
+104
-16
@@ -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 := `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Baihu Panel API Reference</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script id="api-reference" data-url="` + urlPrefix + `/openapi/doc.json"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
</html>`
|
||||
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 := `<script>window.__BASE_URL__ = "` + urlPrefix + `"; window.__API_VERSION__ = "/api/v1";</script>`
|
||||
html = strings.Replace(html, "</head>", configScript+"</head>", 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 := `<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404 Not Found</title></head><body>
|
||||
<script>window.location.href = (window.__BASE_URL__ || "/") + "404";</script>
|
||||
<p>Not Found. Redirecting to home...</p>
|
||||
</body></html>`
|
||||
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||
ctx.Data(status, "text/html", []byte(fallback))
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
html := string(data)
|
||||
|
||||
// 注入配置变量供前端使用(API 调用和路由)
|
||||
configScript := `<script>window.__BASE_URL__ = "` + urlPrefix + `"; window.__API_VERSION__ = "/api/v1";</script>`
|
||||
html = strings.Replace(html, "</head>", configScript+"</head>", 1)
|
||||
|
||||
ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
ctx.Data(status, "text/html; charset=utf-8", []byte(html))
|
||||
ctx.Abort()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user