From 7fd7fb8a2a889dbe971cf18e6e23d1972b0f36c7 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 17 Sep 2026 07:39:21 +0000 Subject: [PATCH] fix(engineering-dashboard): read token from URL/meta under CSP Remove the inline token script blocked by script-src 'self', so the live page no longer calls the snapshot API with token=undefined. Co-authored-by: Cursor --- internal/engineeringdashboard/assets/app.js | 21 +++++++++- .../engineeringdashboard/assets/index.html | 2 +- internal/engineeringdashboard/server_test.go | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/internal/engineeringdashboard/assets/app.js b/internal/engineeringdashboard/assets/app.js index eecb4d03..db7f347c 100644 --- a/internal/engineeringdashboard/assets/app.js +++ b/internal/engineeringdashboard/assets/app.js @@ -1,5 +1,13 @@ (function () { - const token = window.__DASHBOARD_TOKEN__; + function resolveToken() { + const fromURL = new URL(window.location.href).searchParams.get("token"); + if (fromURL) return fromURL; + const meta = document.querySelector('meta[name="engineering-dashboard-token"]'); + const fromMeta = meta && meta.getAttribute("content"); + if (fromMeta && fromMeta !== "__TOKEN__") return fromMeta; + return ""; + } + const token = resolveToken(); const state = { snap: null, view: "overview", @@ -25,7 +33,7 @@ function syncURL() { const u = new URL(window.location.href); - u.searchParams.set("token", token); + if (token) u.searchParams.set("token", token); u.searchParams.set("view", state.view); if (state.q) u.searchParams.set("q", state.q); else u.searchParams.delete("q"); if (state.selectedDoc) u.searchParams.set("doc", state.selectedDoc); else u.searchParams.delete("doc"); @@ -44,6 +52,11 @@ } async function fetchSnapshot(force) { + if (!token) { + el.status.textContent = "缺少访问令牌"; + renderHardError("缺少 token:请使用服务启动时打印的完整 URL(含 ?token=)打开页面"); + return false; + } el.status.textContent = state.snap ? "刷新中…" : "首次加载中…"; const url = "/api/v1/snapshot?token=" + encodeURIComponent(token) + (force ? "&refresh=1" : ""); const headers = {}; @@ -281,6 +294,10 @@ async function loop() { loadURL(); + if (!token) { + renderHardError("缺少 token:请使用服务启动时打印的完整 URL(含 ?token=)打开页面"); + return; + } try { const changed = await fetchSnapshot(true); if (changed || state.snap) render(); diff --git a/internal/engineeringdashboard/assets/index.html b/internal/engineeringdashboard/assets/index.html index 88c76f46..63d53af2 100644 --- a/internal/engineeringdashboard/assets/index.html +++ b/internal/engineeringdashboard/assets/index.html @@ -3,6 +3,7 @@ + 个人工程中控台 @@ -37,7 +38,6 @@
- diff --git a/internal/engineeringdashboard/server_test.go b/internal/engineeringdashboard/server_test.go index d53e4fb8..3d828328 100644 --- a/internal/engineeringdashboard/server_test.go +++ b/internal/engineeringdashboard/server_test.go @@ -128,3 +128,43 @@ func TestServer_CrossOriginDenied(t *testing.T) { t.Fatalf("got %d", rr.Code) } } + +func TestServer_IndexUsesMetaTokenNoInlineScript(t *testing.T) { + c := NewCollector(t.TempDir(), &FakeRunner{}) + c.mu.Lock() + c.current = &Snapshot{RevisionID: "r", GenerationID: "g", GeneratedAt: time.Now().UTC()} + c.mu.Unlock() + srv, err := NewServer(c) + if err != nil { + t.Fatal(err) + } + req := httptest.NewRequest(http.MethodGet, "/?token="+srv.Token, nil) + req.Host = "127.0.0.1:8787" + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("index: %d %s", rr.Code, rr.Body.String()) + } + body := rr.Body.String() + if strings.Contains(body, "window.__DASHBOARD_TOKEN__") { + t.Fatal("index must not use inline token script (CSP script-src 'self')") + } + meta := `name="engineering-dashboard-token" content="` + srv.Token + `"` + if !strings.Contains(body, meta) { + t.Fatalf("missing token meta: %s", body[:min(400, len(body))]) + } + if !strings.Contains(rr.Header().Get("Content-Security-Policy"), "script-src 'self'") { + t.Fatalf("unexpected CSP: %s", rr.Header().Get("Content-Security-Policy")) + } + // app.js must not reference the removed global + js, err := readAsset("assets/app.js") + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(js), "__DASHBOARD_TOKEN__") { + t.Fatal("app.js still references __DASHBOARD_TOKEN__") + } + if !strings.Contains(string(js), "engineering-dashboard-token") { + t.Fatal("app.js should read meta engineering-dashboard-token") + } +}