fix(web-ui): security hardening, SPA nav, and modularization
Ship the in-progress ES-module refactor of the web-ui (new static/modules/ layout, Usage/Settings pages, uplot-based dashboard) alongside a round of security and UX fixes: - main.go: add CSP + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy middleware on every response; WS CheckOrigin now requires Origin host to match Host (blocks cross-site WebSocket hijacking); upgrade client before dialing upstream so origin check runs first; fatal on unparseable AGENTMON_QUERY_BASE. - app.js: delegated click handler intercepts same-origin <a> clicks for SPA navigation (prev. every nav link caused a full page reload, dropping WS + in-memory state); delegated .copy-btn[data-copy] handler replaces inline onclick=; removed window.navigate / window.copyToClipboard globals and the duplicated handleGlobalSearch. - modules/nav-signal.js: per-route AbortController so in-flight fetches are cancelled when the user navigates away, preventing stale toasts and wasted renders. - modules/api.js: honours the nav signal by default; AbortError is silent. - modules/router.js: resets the nav controller on every route; dropped the fixed 80ms transition delay; breadcrumbs no longer emit inline onclick= (delegated handler picks them up). - modules/utils.js: renderCopyButton emits data-copy=\"...\" instead of nesting a JS string inside an HTML attribute — fixes an XSS where values containing ' broke out via ' decoding. Verified: go build clean; `node --check` clean on all modified modules; manual curl probes confirm security headers present on every response and WS upgrade returns 403 for cross-origin/missing Origin while 101 for same-origin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
// ── theme.js — theme toggle, no imports ──────────────────
|
||||
|
||||
export const THEME_CYCLE = ['system', 'light', 'dark'];
|
||||
|
||||
export const THEME_ICONS = {
|
||||
system: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="2" width="14" height="10" rx="1.5"/><path d="M5 15h6M8 12v3"/></svg>',
|
||||
light: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="3"/><line x1="8" y1="1" x2="8" y2="3"/><line x1="8" y1="13" x2="8" y2="15"/><line x1="1" y1="8" x2="3" y2="8"/><line x1="13" y1="8" x2="15" y2="8"/><line x1="3.05" y1="3.05" x2="4.46" y2="4.46"/><line x1="11.54" y1="11.54" x2="12.95" y2="12.95"/><line x1="12.95" y1="3.05" x2="11.54" y2="4.46"/><line x1="4.46" y1="11.54" x2="3.05" y2="12.95"/></svg>',
|
||||
dark: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 10a5 5 0 1 1-7-7 6 6 0 0 0 7 7z"/></svg>',
|
||||
};
|
||||
|
||||
export const THEME_LABELS = { system: 'System theme', light: 'Light theme', dark: 'Dark theme' };
|
||||
|
||||
export function getTheme() { return localStorage.getItem('theme') || 'system'; }
|
||||
|
||||
export function applyTheme(theme) {
|
||||
if (theme === 'system') document.documentElement.removeAttribute('data-theme');
|
||||
else document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
||||
export function updateToggleBtn(theme) {
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (!btn) return;
|
||||
btn.innerHTML = THEME_ICONS[theme];
|
||||
btn.title = THEME_LABELS[theme];
|
||||
}
|
||||
|
||||
export function cycleTheme() {
|
||||
const next = THEME_CYCLE[(THEME_CYCLE.indexOf(getTheme()) + 1) % THEME_CYCLE.length];
|
||||
if (next === 'system') localStorage.removeItem('theme');
|
||||
else localStorage.setItem('theme', next);
|
||||
applyTheme(next);
|
||||
updateToggleBtn(next);
|
||||
}
|
||||
Reference in New Issue
Block a user