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,49 @@
|
||||
import { app } from '../router.js';
|
||||
import { showToast } from '../api.js';
|
||||
|
||||
export async function renderSettings() {
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">Data Retention</h3>
|
||||
<p class="settings-section-desc">Delete events older than the specified number of days. This runs automatically every 24 hours. Currently configured via <code>RETENTION_DAYS</code> environment variable (default: 30 days).</p>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label" for="retention-days">Purge events older than</label>
|
||||
<div class="settings-input-group">
|
||||
<input type="number" id="retention-days" class="settings-input" min="1" max="365" value="30">
|
||||
<span class="settings-input-suffix">days</span>
|
||||
<button class="settings-btn" id="run-retention-btn" type="button">Run Now</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="retention-result" class="settings-result"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('run-retention-btn')?.addEventListener('click', async () => {
|
||||
const days = parseInt(document.getElementById('retention-days').value, 10);
|
||||
if (!days || days < 1) { showToast('Enter a valid number of days', 'error'); return; }
|
||||
const btn = document.getElementById('run-retention-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Running…';
|
||||
try {
|
||||
const resp = await fetch('/api/v1/admin/retention', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ days }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || 'Request failed');
|
||||
const result = document.getElementById('retention-result');
|
||||
if (result) result.innerHTML = `<span class="settings-result-ok">Deleted ${data.deleted} events older than ${new Date(data.cutoff).toLocaleDateString()}.</span>`;
|
||||
showToast(`Deleted ${data.deleted} events`, 'success');
|
||||
} catch (e) {
|
||||
showToast('Retention failed: ' + e.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Run Now';
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user