admin: /admin HTML 公开可访问,让 token 弹窗真正能触发
上一轮 (90eba22) 把 admin 页改成 token 弹窗模式,但 webHandler 还
挂在带 AdminAuth 的 group 里。结果无 token 访问 /admin 拿到的不是
HTML,而是 {"error":"missing bearer token"} 的 401 JSON,弹窗
变成死代码。
修复: 拆出 gPublic group,只放 webHandler (GET /admin 和 /admin/)。
API 路由 (/clusters*, /audit) 和 OpenAPI docs (/docs, /docs/spec)
继续受 AdminAuth 保护 (后者暴露内部 API 结构,不该公网可见)。
HTML 自身的 token 输入流不变 — 弹窗仍然在客户端拦截 401、清旧 token、
记录 pendingRetry、保存新 token 后自动重试。
补一个 TestAdminWeb_NoTokenReturnsHTML 锁定新契约:
- GET /admin 无 token -> 200 + text/html (含 token-modal 元素)
- GET /admin/ 同上
- GET /admin/clusters 无 token -> 401 (回归保护)
- GET /admin/clusters good-token -> 200 (回归保护)
旧 TestMount_RequiresAuth 测的全是 API 路径, 不受影响, 全绿。
验证:
- go build / go vet / go test 全部通过
- 端到端 curl 复现用户报告场景: /admin 无 token -> 200 + 弹窗 HTML
- API + docs 鉴权行为不变
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,9 +19,13 @@ import (
|
|||||||
// Mount attaches the /admin sub-router to r, protecting every route with
|
// Mount attaches the /admin sub-router to r, protecting every route with
|
||||||
// bearer-token admin authentication.
|
// bearer-token admin authentication.
|
||||||
func Mount(r *gin.Engine, repo *storage.ClusterRepo, auditRepo *audit.Repo, adminTokens []string) {
|
func Mount(r *gin.Engine, repo *storage.ClusterRepo, auditRepo *audit.Repo, adminTokens []string) {
|
||||||
|
// HTML page is public — its own modal prompts for the token.
|
||||||
|
// All other /admin/* endpoints (API + OpenAPI docs) still require auth.
|
||||||
|
gPublic := r.Group("/admin")
|
||||||
|
gPublic.GET("", webHandler)
|
||||||
|
gPublic.GET("/", webHandler)
|
||||||
|
|
||||||
g := r.Group("/admin", middleware.AdminAuth(adminTokens))
|
g := r.Group("/admin", middleware.AdminAuth(adminTokens))
|
||||||
g.GET("", webHandler)
|
|
||||||
g.GET("/", webHandler)
|
|
||||||
g.GET("/clusters", listClusters(repo))
|
g.GET("/clusters", listClusters(repo))
|
||||||
g.POST("/clusters", createCluster(repo, auditRepo))
|
g.POST("/clusters", createCluster(repo, auditRepo))
|
||||||
g.GET("/clusters/:id", getCluster(repo))
|
g.GET("/clusters/:id", getCluster(repo))
|
||||||
|
|||||||
@@ -96,6 +96,45 @@ func TestMount_RequiresAuth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdminWeb_NoTokenReturnsHTML(t *testing.T) {
|
||||||
|
r, _, _ := newTestAdmin(t)
|
||||||
|
|
||||||
|
// 1) /admin without token: 200 + HTML
|
||||||
|
w := doReq(t, r, "GET", "/admin", "", nil)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /admin without token: got %d, want 200", w.Code)
|
||||||
|
}
|
||||||
|
ct := w.Header().Get("Content-Type")
|
||||||
|
if !strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q, want text/html", ct)
|
||||||
|
}
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "token-modal") {
|
||||||
|
t.Errorf("body missing token-modal element; body excerpt: %.200s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "Admin Token Required") {
|
||||||
|
t.Errorf("body missing modal title")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) /admin/ (trailing slash) same behavior
|
||||||
|
w2 := doReq(t, r, "GET", "/admin/", "", nil)
|
||||||
|
if w2.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /admin/ without token: got %d, want 200", w2.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) API routes still require auth (no token = 401)
|
||||||
|
w3 := doReq(t, r, "GET", "/admin/clusters", "", nil)
|
||||||
|
if w3.Code != http.StatusUnauthorized {
|
||||||
|
t.Errorf("GET /admin/clusters without token: got %d, want 401", w3.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) API routes still require auth (valid token = 200)
|
||||||
|
w4 := doReq(t, r, "GET", "/admin/clusters", "good-token", nil)
|
||||||
|
if w4.Code != http.StatusOK {
|
||||||
|
t.Errorf("GET /admin/clusters with good-token: got %d, want 200", w4.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListClusters(t *testing.T) {
|
func TestListClusters(t *testing.T) {
|
||||||
r, _, _ := newTestAdmin(t)
|
r, _, _ := newTestAdmin(t)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user