diff --git a/internal/admin/router.go b/internal/admin/router.go index 9447f73..0c683d4 100644 --- a/internal/admin/router.go +++ b/internal/admin/router.go @@ -19,9 +19,13 @@ import ( // Mount attaches the /admin sub-router to r, protecting every route with // bearer-token admin authentication. 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.GET("", webHandler) - g.GET("/", webHandler) g.GET("/clusters", listClusters(repo)) g.POST("/clusters", createCluster(repo, auditRepo)) g.GET("/clusters/:id", getCluster(repo)) diff --git a/internal/admin/router_test.go b/internal/admin/router_test.go index d58294d..e01b6d0 100644 --- a/internal/admin/router_test.go +++ b/internal/admin/router_test.go @@ -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) { r, _, _ := newTestAdmin(t)