Skip to content

Commit f026e38

Browse files
committed
middleware: allow public admin entry/static paths in CombinedAdminAuth to avoid blocking login page
1 parent 499801b commit f026e38

File tree

10 files changed

+335
-183
lines changed

10 files changed

+335
-183
lines changed

internal/handlers/setup.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ var OnDatabaseInitialized func(daoManager *repository.RepositoryManager)
182182
// initInProgress 用于防止并发初始化
183183
var initInProgress int32 = 0
184184

185+
// onDBInitCalled 防止重复调用 OnDatabaseInitialized(多次 POST /setup 导致重复注册路由)
186+
var onDBInitCalled int32 = 0
187+
185188
// InitializeNoDB 用于在没有 daoManager 的情况下处理 /setup/initialize 请求
186189
// 它会:验证请求、使用配置管理器初始化数据库、创建 daoManager、创建管理员用户,最后触发 OnDatabaseInitialized 回调
187190
func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc {
@@ -394,13 +397,18 @@ func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc {
394397

395398
// 触发回调以让主程序挂载其余路由并启动后台任务
396399
if OnDatabaseInitialized != nil {
397-
OnDatabaseInitialized(daoManager)
400+
// 只允许调用一次,避免重复注册路由导致 gin panic
401+
if atomic.CompareAndSwapInt32(&onDBInitCalled, 0, 1) {
402+
OnDatabaseInitialized(daoManager)
403+
} else {
404+
log.Printf("[InitializeNoDB] OnDatabaseInitialized 已调用,跳过重复挂载")
405+
}
398406
}
399407

400408
common.SuccessWithMessage(c, "系统初始化成功", map[string]interface{}{
401-
"message": "系统初始化完成",
402-
"admin_username": req.Admin.Username,
403-
"database_type": req.Database.Type,
409+
"message": "系统初始化完成",
410+
"username": req.Admin.Username,
411+
"database_type": req.Database.Type,
404412
})
405413
}
406414
}
@@ -467,7 +475,7 @@ func (h *SetupHandler) Initialize(c *gin.Context) {
467475
}
468476

469477
common.SuccessWithMessage(c, "系统初始化成功", map[string]interface{}{
470-
"message": "系统初始化完成",
471-
"admin_username": req.Admin.Username,
478+
"message": "系统初始化完成",
479+
"username": req.Admin.Username,
472480
})
473481
}

internal/middleware/combined_auth.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ func CombinedAdminAuth(manager *config.ConfigManager, userService interface {
1515
ValidateToken(string) (interface{}, error)
1616
}) gin.HandlerFunc {
1717
return func(c *gin.Context) {
18+
// Allow public access to admin front-end entry and static assets
19+
// This prevents accidental interception of requests for the login page
20+
// when an invalid/stale Authorization header is present.
21+
p := c.Request.URL.Path
22+
if p == "/admin/" || p == "/admin" ||
23+
strings.HasPrefix(p, "/admin/css/") || strings.HasPrefix(p, "/admin/js/") ||
24+
strings.HasPrefix(p, "/admin/assets/") || strings.HasPrefix(p, "/admin/templates/") ||
25+
strings.HasPrefix(p, "/admin/components/") {
26+
c.Next()
27+
return
28+
}
1829
// 先尝试JWT用户认证
1930
authHeader := c.GetHeader("Authorization")
2031
if authHeader != "" {

internal/routes/admin.go

Lines changed: 122 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -54,142 +54,135 @@ func SetupAdminRoutes(
5454

5555
}
5656

57+
// 将管理后台静态资源与前端入口注册为公开路由,允许未认证用户加载登录页面和相关静态资源
58+
// 注意:API 路由仍然放在受保护的 authGroup 中
59+
themeDir := "./" + cfg.ThemesSelect
60+
61+
// css
62+
adminGroup.GET("/css/*filepath", func(c *gin.Context) {
63+
fp := c.Param("filepath")
64+
p := filepath.Join(themeDir, "admin", "css", fp)
65+
if _, err := os.Stat(p); err != nil {
66+
c.Status(404)
67+
return
68+
}
69+
c.File(p)
70+
})
71+
72+
// HEAD for css
73+
adminGroup.HEAD("/css/*filepath", func(c *gin.Context) {
74+
fp := c.Param("filepath")
75+
p := filepath.Join(themeDir, "admin", "css", fp)
76+
if _, err := os.Stat(p); err != nil {
77+
c.Status(404)
78+
return
79+
}
80+
c.File(p)
81+
})
82+
83+
// js
84+
adminGroup.GET("/js/*filepath", func(c *gin.Context) {
85+
fp := c.Param("filepath")
86+
p := filepath.Join(themeDir, "admin", "js", fp)
87+
if _, err := os.Stat(p); err != nil {
88+
c.Status(404)
89+
return
90+
}
91+
c.File(p)
92+
})
93+
94+
// HEAD for js
95+
adminGroup.HEAD("/js/*filepath", func(c *gin.Context) {
96+
fp := c.Param("filepath")
97+
p := filepath.Join(themeDir, "admin", "js", fp)
98+
if _, err := os.Stat(p); err != nil {
99+
c.Status(404)
100+
return
101+
}
102+
c.File(p)
103+
})
104+
105+
// templates
106+
adminGroup.GET("/templates/*filepath", func(c *gin.Context) {
107+
fp := c.Param("filepath")
108+
p := filepath.Join(themeDir, "admin", "templates", fp)
109+
if _, err := os.Stat(p); err != nil {
110+
c.Status(404)
111+
return
112+
}
113+
c.File(p)
114+
})
115+
116+
// HEAD for templates
117+
adminGroup.HEAD("/templates/*filepath", func(c *gin.Context) {
118+
fp := c.Param("filepath")
119+
p := filepath.Join(themeDir, "admin", "templates", fp)
120+
if _, err := os.Stat(p); err != nil {
121+
c.Status(404)
122+
return
123+
}
124+
c.File(p)
125+
})
126+
127+
// assets and components
128+
adminGroup.GET("/assets/*filepath", func(c *gin.Context) {
129+
fp := c.Param("filepath")
130+
p := filepath.Join(themeDir, "assets", fp)
131+
if _, err := os.Stat(p); err != nil {
132+
c.Status(404)
133+
return
134+
}
135+
c.File(p)
136+
})
137+
138+
// HEAD for assets
139+
adminGroup.HEAD("/assets/*filepath", func(c *gin.Context) {
140+
fp := c.Param("filepath")
141+
p := filepath.Join(themeDir, "assets", fp)
142+
if _, err := os.Stat(p); err != nil {
143+
c.Status(404)
144+
return
145+
}
146+
c.File(p)
147+
})
148+
149+
adminGroup.GET("/components/*filepath", func(c *gin.Context) {
150+
fp := c.Param("filepath")
151+
p := filepath.Join(themeDir, "components", fp)
152+
if _, err := os.Stat(p); err != nil {
153+
c.Status(404)
154+
return
155+
}
156+
c.File(p)
157+
})
158+
159+
// HEAD for components
160+
adminGroup.HEAD("/components/*filepath", func(c *gin.Context) {
161+
fp := c.Param("filepath")
162+
p := filepath.Join(themeDir, "components", fp)
163+
if _, err := os.Stat(p); err != nil {
164+
c.Status(404)
165+
return
166+
}
167+
c.File(p)
168+
})
169+
170+
// 管理前端入口公开:允许未认证用户加载登录页面
171+
adminGroup.GET("/", func(c *gin.Context) {
172+
static.ServeAdminPage(c, cfg)
173+
})
174+
// HEAD for admin entry
175+
adminGroup.HEAD("/", func(c *gin.Context) {
176+
static.ServeAdminPage(c, cfg)
177+
})
178+
57179
// 使用复用的中间件实现(JWT 用户认证并要求 admin 角色)
58180
combinedAuthMiddleware := middleware.CombinedAdminAuth(cfg, userService)
59181

60182
// 需要管理员认证的API路由组
61183
authGroup := adminGroup.Group("")
62184
authGroup.Use(combinedAuthMiddleware)
63185
{
64-
// 显式为管理后台静态资源注册受保护的 GET 处理器,确保这些静态文件也需要管理员认证
65-
themeDir := "./" + cfg.ThemesSelect
66-
67-
// css
68-
authGroup.GET("/css/*filepath", func(c *gin.Context) {
69-
fp := c.Param("filepath")
70-
p := filepath.Join(themeDir, "admin", "css", fp)
71-
if _, err := os.Stat(p); err != nil {
72-
c.Status(404)
73-
return
74-
}
75-
c.File(p)
76-
})
77-
78-
// HEAD for css (ensure middleware runs for HEAD as well)
79-
authGroup.HEAD("/css/*filepath", func(c *gin.Context) {
80-
fp := c.Param("filepath")
81-
p := filepath.Join(themeDir, "admin", "css", fp)
82-
if _, err := os.Stat(p); err != nil {
83-
c.Status(404)
84-
return
85-
}
86-
c.File(p)
87-
})
88-
89-
// js
90-
authGroup.GET("/js/*filepath", func(c *gin.Context) {
91-
fp := c.Param("filepath")
92-
p := filepath.Join(themeDir, "admin", "js", fp)
93-
if _, err := os.Stat(p); err != nil {
94-
c.Status(404)
95-
return
96-
}
97-
c.File(p)
98-
})
99-
100-
// HEAD for js
101-
authGroup.HEAD("/js/*filepath", func(c *gin.Context) {
102-
fp := c.Param("filepath")
103-
p := filepath.Join(themeDir, "admin", "js", fp)
104-
if _, err := os.Stat(p); err != nil {
105-
c.Status(404)
106-
return
107-
}
108-
c.File(p)
109-
})
110-
111-
// templates
112-
authGroup.GET("/templates/*filepath", func(c *gin.Context) {
113-
fp := c.Param("filepath")
114-
p := filepath.Join(themeDir, "admin", "templates", fp)
115-
if _, err := os.Stat(p); err != nil {
116-
c.Status(404)
117-
return
118-
}
119-
c.File(p)
120-
})
121-
122-
// HEAD for templates
123-
authGroup.HEAD("/templates/*filepath", func(c *gin.Context) {
124-
fp := c.Param("filepath")
125-
p := filepath.Join(themeDir, "admin", "templates", fp)
126-
if _, err := os.Stat(p); err != nil {
127-
c.Status(404)
128-
return
129-
}
130-
c.File(p)
131-
})
132-
133-
// assets and components
134-
authGroup.GET("/assets/*filepath", func(c *gin.Context) {
135-
fp := c.Param("filepath")
136-
p := filepath.Join(themeDir, "assets", fp)
137-
if _, err := os.Stat(p); err != nil {
138-
c.Status(404)
139-
return
140-
}
141-
c.File(p)
142-
})
143-
144-
// HEAD for assets
145-
authGroup.HEAD("/assets/*filepath", func(c *gin.Context) {
146-
fp := c.Param("filepath")
147-
p := filepath.Join(themeDir, "assets", fp)
148-
if _, err := os.Stat(p); err != nil {
149-
c.Status(404)
150-
return
151-
}
152-
c.File(p)
153-
})
154-
authGroup.GET("/components/*filepath", func(c *gin.Context) {
155-
fp := c.Param("filepath")
156-
p := filepath.Join(themeDir, "components", fp)
157-
if _, err := os.Stat(p); err != nil {
158-
c.Status(404)
159-
return
160-
}
161-
c.File(p)
162-
})
163-
164-
// HEAD for components
165-
authGroup.HEAD("/components/*filepath", func(c *gin.Context) {
166-
fp := c.Param("filepath")
167-
p := filepath.Join(themeDir, "components", fp)
168-
if _, err := os.Stat(p); err != nil {
169-
c.Status(404)
170-
return
171-
}
172-
c.File(p)
173-
})
174-
authGroup.GET("/components/*filepath", func(c *gin.Context) {
175-
fp := c.Param("filepath")
176-
p := filepath.Join(themeDir, "components", fp)
177-
if _, err := os.Stat(p); err != nil {
178-
c.Status(404)
179-
return
180-
}
181-
c.File(p)
182-
})
183-
184-
// 管理前端入口受保护:仅管理员可访问 /admin/
185-
authGroup.GET("/", func(c *gin.Context) {
186-
static.ServeAdminPage(c, cfg)
187-
})
188-
// HEAD for admin entry
189-
authGroup.HEAD("/", func(c *gin.Context) {
190-
static.ServeAdminPage(c, cfg)
191-
})
192-
193186
// 仪表板和统计
194187
authGroup.GET("/dashboard", adminHandler.Dashboard)
195188
authGroup.GET("/stats", adminHandler.GetStats)

internal/routes/setup.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import (
1515
"github.com/zy84338719/filecodebox/internal/static"
1616
"github.com/zy84338719/filecodebox/internal/storage"
1717

18+
"sync"
19+
"sync/atomic"
20+
1821
"github.com/gin-gonic/gin"
1922
"github.com/sirupsen/logrus"
2023
)
@@ -168,6 +171,22 @@ func RegisterDynamicRoutes(
168171
daoManager *repository.RepositoryManager,
169172
storageManager *storage.StorageManager,
170173
) {
174+
// 序列化注册,防止并发导致的重复注册 panic
175+
dynamicRegisterMu.Lock()
176+
defer dynamicRegisterMu.Unlock()
177+
178+
// 使用原子标志防止重复调用
179+
if registerDynamicOnce() {
180+
logrus.Info("动态路由已注册(atomic),跳过 RegisterDynamicRoutes")
181+
return
182+
}
183+
// 如果动态路由已经注册(例如 /share/text/ 已存在),则跳过注册以防止重复注册导致 panic
184+
for _, r := range router.Routes() {
185+
if r.Method == "POST" && r.Path == "/share/text/" {
186+
logrus.Info("动态路由已存在,跳过 RegisterDynamicRoutes")
187+
return
188+
}
189+
}
171190
// 创建具体的存储服务
172191
storageService := storage.NewConcreteStorageService(manager)
173192

@@ -195,6 +214,21 @@ func RegisterDynamicRoutes(
195214
// System init routes are no longer needed after DB init
196215
}
197216

217+
// package-level atomic to ensure RegisterDynamicRoutes runs only once
218+
var dynamicRoutesRegistered int32 = 0
219+
220+
func registerDynamicOnce() bool {
221+
// 如果已经设置,则返回 true
222+
if atomic.LoadInt32(&dynamicRoutesRegistered) == 1 {
223+
return true
224+
}
225+
// 尝试设置为 1
226+
return !atomic.CompareAndSwapInt32(&dynamicRoutesRegistered, 0, 1)
227+
}
228+
229+
// mutex to serialize dynamic route registration
230+
var dynamicRegisterMu sync.Mutex
231+
198232
// SetupAllRoutes 设置所有路由(使用已初始化的处理器)
199233
func SetupAllRoutes(
200234
router *gin.Engine,

internal/routes/share.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ func SetupShareRoutes(
1717
ValidateToken(string) (interface{}, error)
1818
},
1919
) {
20+
// 幂等检查:如果 /share/text/ 已注册则跳过(防止重复注册导致 gin panic)
21+
for _, r := range router.Routes() {
22+
if r.Method == "POST" && r.Path == "/share/text/" {
23+
return
24+
}
25+
}
2026
// 分享相关路由
2127
shareGroup := router.Group("/share")
2228
shareGroup.Use(middleware.ShareAuth(cfg))

0 commit comments

Comments
 (0)