Skip to content

Commit bc6ad77

Browse files
committed
Enforce setup gating and align admin UI theme
1 parent 68407df commit bc6ad77

File tree

7 files changed

+847
-366
lines changed

7 files changed

+847
-366
lines changed

internal/handlers/setup.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,41 @@ func (h *SetupHandler) enableUserSystem(adminConfig AdminConfig) error {
166166
})
167167
}
168168

169+
func (h *SetupHandler) isSystemInitialized() (bool, error) {
170+
return isSystemInitialized(h.manager, h.daoManager)
171+
}
172+
173+
func isSystemInitialized(manager *config.ConfigManager, daoManager *repository.RepositoryManager) (bool, error) {
174+
if daoManager != nil && daoManager.User != nil {
175+
count, err := daoManager.User.CountAdminUsers()
176+
if err != nil {
177+
return false, err
178+
}
179+
return count > 0, nil
180+
}
181+
182+
if manager == nil {
183+
return false, nil
184+
}
185+
186+
db := manager.GetDB()
187+
if db == nil {
188+
return false, nil
189+
}
190+
191+
repo := repository.NewRepositoryManager(db)
192+
if repo.User == nil {
193+
return false, nil
194+
}
195+
196+
count, err := repo.User.CountAdminUsers()
197+
if err != nil {
198+
return false, err
199+
}
200+
201+
return count > 0, nil
202+
}
203+
169204
// contains 检查字符串是否包含子字符串
170205
func contains(s, substr string) bool {
171206
for i := 0; i <= len(s)-len(substr); i++ {
@@ -197,6 +232,17 @@ func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc {
197232
return
198233
}
199234
defer atomic.StoreInt32(&initInProgress, 0)
235+
236+
initialized, err := isSystemInitialized(manager, nil)
237+
if err != nil {
238+
logrus.WithError(err).Error("[InitializeNoDB] 检查系统初始化状态失败")
239+
common.InternalServerErrorResponse(c, "检查系统初始化状态失败")
240+
return
241+
}
242+
if initialized {
243+
common.ForbiddenResponse(c, "系统已初始化,禁止重复初始化")
244+
return
245+
}
200246
// 解析 JSON(仅接受嵌套结构),不再兼容 legacy 扁平字段
201247
var req SetupRequest
202248
if !utils.BindJSONWithValidation(c, &req) {
@@ -431,6 +477,17 @@ func (h *SetupHandler) Initialize(c *gin.Context) {
431477
return
432478
}
433479

480+
initialized, err := h.isSystemInitialized()
481+
if err != nil {
482+
logrus.WithError(err).Error("[SetupHandler.Initialize] 检查系统初始化状态失败")
483+
common.InternalServerErrorResponse(c, "检查系统初始化状态失败")
484+
return
485+
}
486+
if initialized {
487+
common.ForbiddenResponse(c, "系统已初始化,禁止重复初始化")
488+
return
489+
}
490+
434491
var req SetupRequest
435492
if !utils.BindJSONWithValidation(c, &req) {
436493
return

internal/middleware/setup_guard.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
"github.com/gin-gonic/gin"
8+
"github.com/sirupsen/logrus"
9+
)
10+
11+
// SetupGuardConfig controls how the setup guard middleware behaves.
12+
type SetupGuardConfig struct {
13+
// IsInitialized returns the current initialization status.
14+
IsInitialized func() (bool, error)
15+
// SetupPath denotes the setup entry path, defaults to /setup.
16+
SetupPath string
17+
// RedirectPath denotes the path to redirect to once initialized, defaults to /.
18+
RedirectPath string
19+
// AllowPaths lists exact paths that should remain accessible before initialization.
20+
AllowPaths []string
21+
// AllowPrefixes lists path prefixes that should remain accessible before initialization.
22+
AllowPrefixes []string
23+
}
24+
25+
// SetupGuard ensures only setup resources are accessible before initialization
26+
// and blocks setup routes after initialization is complete.
27+
func SetupGuard(cfg SetupGuardConfig) gin.HandlerFunc {
28+
setupPath := cfg.SetupPath
29+
if setupPath == "" {
30+
setupPath = "/setup"
31+
}
32+
redirectPath := cfg.RedirectPath
33+
if redirectPath == "" {
34+
redirectPath = "/"
35+
}
36+
37+
allowPaths := map[string]struct{}{
38+
setupPath: {},
39+
setupPath + "/": {},
40+
}
41+
42+
for _, p := range cfg.AllowPaths {
43+
allowPaths[p] = struct{}{}
44+
}
45+
46+
allowPrefixes := []string{setupPath + "/"}
47+
allowPrefixes = append(allowPrefixes, cfg.AllowPrefixes...)
48+
49+
return func(c *gin.Context) {
50+
initialized := false
51+
if cfg.IsInitialized != nil {
52+
var err error
53+
initialized, err = cfg.IsInitialized()
54+
if err != nil {
55+
logrus.WithError(err).Warn("setup guard: failed to determine initialization state")
56+
// Fail closed on error so users can still reach setup for recovery.
57+
initialized = false
58+
}
59+
}
60+
61+
path := c.Request.URL.Path
62+
63+
if initialized {
64+
if path == setupPath || strings.HasPrefix(path, setupPath+"/") {
65+
switch c.Request.Method {
66+
case http.MethodGet, http.MethodHead:
67+
c.Redirect(http.StatusFound, redirectPath)
68+
case http.MethodOptions:
69+
c.Status(http.StatusNoContent)
70+
default:
71+
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
72+
"code": http.StatusForbidden,
73+
"message": "系统已初始化,禁止重新初始化",
74+
})
75+
}
76+
c.Abort()
77+
return
78+
}
79+
80+
c.Next()
81+
return
82+
}
83+
84+
if _, ok := allowPaths[path]; ok {
85+
c.Next()
86+
return
87+
}
88+
for _, prefix := range allowPrefixes {
89+
if strings.HasPrefix(path, prefix) {
90+
c.Next()
91+
return
92+
}
93+
}
94+
95+
switch c.Request.Method {
96+
case http.MethodGet, http.MethodHead:
97+
c.Redirect(http.StatusFound, setupPath)
98+
case http.MethodOptions:
99+
// Allow CORS preflight to complete without redirect loops.
100+
c.Status(http.StatusNoContent)
101+
default:
102+
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
103+
"code": http.StatusForbidden,
104+
"message": "系统未初始化,请访问 /setup 完成初始化",
105+
})
106+
}
107+
c.Abort()
108+
}
109+
}

internal/routes/setup.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,71 @@ func CreateAndSetupRouter(
9090
router.Use(middleware.CORS())
9191
router.Use(middleware.RateLimit(manager))
9292

93+
var cachedInitialized atomic.Bool
94+
var cachedRepo atomic.Pointer[repository.RepositoryManager]
95+
96+
guardConfig := middleware.SetupGuardConfig{
97+
SetupPath: "/setup",
98+
RedirectPath: "/",
99+
AllowPaths: []string{
100+
"/setup/initialize",
101+
"/check-init",
102+
"/user/system-info",
103+
"/health",
104+
},
105+
AllowPrefixes: []string{
106+
"/assets/",
107+
"/css/",
108+
"/js/",
109+
"/components/",
110+
},
111+
}
112+
113+
guardConfig.IsInitialized = func() (bool, error) {
114+
if cachedInitialized.Load() {
115+
return true, nil
116+
}
117+
118+
if daoManager != nil && daoManager.User != nil {
119+
count, err := daoManager.User.CountAdminUsers()
120+
if err != nil {
121+
return false, err
122+
}
123+
if count > 0 {
124+
cachedInitialized.Store(true)
125+
return true, nil
126+
}
127+
return false, nil
128+
}
129+
130+
db := manager.GetDB()
131+
if db == nil {
132+
return false, nil
133+
}
134+
135+
repo := cachedRepo.Load()
136+
if repo == nil || repo.DB() != db {
137+
repo = repository.NewRepositoryManager(db)
138+
cachedRepo.Store(repo)
139+
}
140+
if repo.User == nil {
141+
return false, nil
142+
}
143+
144+
count, err := repo.User.CountAdminUsers()
145+
if err != nil {
146+
return false, err
147+
}
148+
149+
if count > 0 {
150+
cachedInitialized.Store(true)
151+
return true, nil
152+
}
153+
return false, nil
154+
}
155+
156+
router.Use(middleware.SetupGuard(guardConfig))
157+
93158
// 如果 daoManager 为 nil,表示尚未初始化数据库,只注册基础和初始化相关的路由
94159
if daoManager == nil {
95160
// 基础路由(不传 userHandler)

0 commit comments

Comments
 (0)