mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +00:00
fix(webui): version display, CSP guards, config traps, bucket-aligned stacking
Review fix round (4 Important + 2 folded): 1. Version display: OverviewView.vue was missing the muted version line the old vanilla-JS page rendered (types/api.ts already declared overview.version) - added. 2. CSP invariant now guarded twice: webui/generate.mjs's new assertNoInlineAssets fails the build if dist/index.html contains an inline <script> (no src= attribute) or any <style> tag - the exact condition the tightened admin CSP (script-src/style-src 'self', no 'unsafe-inline') depends on. admin_test.go's new TestAdminIndexHTML_NoInlineAssets re-asserts the identical invariant server-side, against the decoded adminIndexHTML admin.go actually serves, so a regression is caught even if admin_assets_gen.go were regenerated with a modified copy of generate.mjs. 3. webui/components.json's iconLibrary:"lucide"/font:"inter" fields cannot be fixed in place - checked shadcn-vue's own schema (PRESET_ICON_LIBRARIES/PRESET_FONTS in the installed package): no "none"/fontawesome/system choice exists for either, and JSON (unlike the tsconfig.*.json files touched in the previous commit) has no comment syntax to warn inline. Documented instead: webui/README.md gained a loud warning section naming the exact regression (already hit twice this branch - a re-added Google Fonts import, a re-added lucide icon import) and the two-step fix to apply after any future shadcn-vue add/init call. 4. Real-CSP verification: the browser pass earlier in this branch ran under vite preview, which sends no CSP header at all. New integration/integration_test.go: TestAdminAssetCSP fetches GET /admin against the real Traefik+Yaegi pipeline, extracts the asset URL from its <script src="..."> tag, fetches that asset, and asserts both responses carry the exact adminCSP string byte-for-byte. admin.go's serveAdminAsset now also sets Content-Security-Policy (previously JSON/page routes only) so this holds for a direct navigation to an asset URL, not just as a same-origin sub-resource of GET /admin; admin_test.go's TestAdminAsset_Serving updated to match. 5. (folded) UsageChart.vue stacked the "tokens" tab's two series (tokin/tokout - two separate HTTP requests, not one atomic call) by positional index. A window rollover landing between the two requests could leave one series one bucket ahead, mis-stacking that refresh. Now aligns both series by their bucket key (a sorted union of every active series' buckets - the fixed-width, zero-padded bucket strings sort chronologically by construction), with a missing bucket in one series reading as 0 rather than shifting the whole series. 6. (folded) AuthGate.vue now triggers an immediate dashboard.refresh() on successful key submit, instead of waiting up to 5s for the next poll tick - the dashboard no longer sits blank right after auth. Rebuilt via make admin-ui; admin_assets_gen.go regenerated.
This commit is contained in:
@@ -629,6 +629,9 @@ func TestAdminAsset_Serving(t *testing.T) {
|
||||
if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" {
|
||||
t.Errorf("X-Content-Type-Options = %q, want %q", got, "nosniff")
|
||||
}
|
||||
if got := rec.Header().Get("Content-Security-Policy"); got != adminCSP {
|
||||
t.Errorf("Content-Security-Policy = %q, want %q", got, adminCSP)
|
||||
}
|
||||
cc := rec.Header().Get("Cache-Control")
|
||||
if !strings.Contains(cc, "immutable") || !strings.Contains(cc, "max-age=31536000") {
|
||||
t.Errorf("Cache-Control = %q, want a long-lived immutable directive", cc)
|
||||
@@ -692,6 +695,36 @@ func TestAdminAssets_GeneratedFilePresent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// adminInlineScriptRe matches every <script ...> opening tag, capturing its
|
||||
// attribute text — TestAdminIndexHTML_NoInlineAssets checks each capture for
|
||||
// a src= attribute.
|
||||
var adminInlineScriptRe = regexp.MustCompile(`(?i)<script\b([^>]*)>`)
|
||||
|
||||
// TestAdminIndexHTML_NoInlineAssets asserts, server-side, the exact CSP
|
||||
// invariant generate.mjs's own assertNoInlineAssets checks at build time
|
||||
// (webui/generate.mjs): the decoded adminIndexHTML — what admin.go's
|
||||
// serveAdminPage actually serves — contains no inline <script> (a
|
||||
// <script> tag with no src= attribute) and no <style> tag anywhere. The
|
||||
// admin CSP (adminCSP, above) ships script-src/style-src 'self' with no
|
||||
// 'unsafe-inline'; either violation would silently break under that
|
||||
// policy. This test exists as a second, independent guard: it catches a
|
||||
// regression even if admin_assets_gen.go were regenerated with a
|
||||
// modified copy of generate.mjs that dropped or weakened its own check.
|
||||
func TestAdminIndexHTML_NoInlineAssets(t *testing.T) {
|
||||
t.Parallel()
|
||||
html := string(adminIndexHTML)
|
||||
|
||||
for _, m := range adminInlineScriptRe.FindAllStringSubmatch(html, -1) {
|
||||
attrs := m[1]
|
||||
if !strings.Contains(strings.ToLower(attrs), "src=") {
|
||||
t.Errorf("adminIndexHTML contains an inline <script> (no src= attribute): %q", m[0])
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(html), "<style") {
|
||||
t.Error("adminIndexHTML contains a <style> tag — every style must be an external <link rel=\"stylesheet\">")
|
||||
}
|
||||
}
|
||||
|
||||
func truncateForTest(b []byte, n int) string {
|
||||
if len(b) <= n {
|
||||
return string(b)
|
||||
|
||||
Reference in New Issue
Block a user