From c0e8faa30783ae154dc5b23079255ed87905388d Mon Sep 17 00:00:00 2001 From: Polliog Date: Sat, 20 Jun 2026 21:34:03 +0200 Subject: [PATCH 01/10] gitignore frontend bug-hunt reports --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 10eb9c2e..f0408e28 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ claude.md /test-vector/ /.env.prod /docs/superpowers/ +/FRONTEND-BUGS.md +/BUGS.md From 9fb2a9a1ce34009fa5e1dfab0f65eedb1938031f Mon Sep 17 00:00:00 2001 From: Polliog Date: Sat, 20 Jun 2026 21:34:03 +0200 Subject: [PATCH 02/10] fix frontend api/store error handling, races and leaks --- packages/frontend/src/lib/api/admin.ts | 10 +++--- packages/frontend/src/lib/api/auth.ts | 36 ++++++++++++++----- packages/frontend/src/lib/api/exceptions.ts | 32 ++++++++--------- packages/frontend/src/lib/stores/auth.ts | 33 ++++++++++++++--- .../src/lib/stores/custom-dashboards.ts | 15 ++++++++ .../frontend/src/lib/stores/monitoring.ts | 9 +++++ .../src/lib/stores/notification-channels.ts | 5 +-- packages/frontend/src/lib/utils/siem.ts | 4 +-- 8 files changed, 105 insertions(+), 39 deletions(-) diff --git a/packages/frontend/src/lib/api/admin.ts b/packages/frontend/src/lib/api/admin.ts index b341681a..75880776 100644 --- a/packages/frontend/src/lib/api/admin.ts +++ b/packages/frontend/src/lib/api/admin.ts @@ -439,7 +439,7 @@ class AdminAPI { } if (!response.ok) { - const error = await response.json(); + const error = await response.json().catch(() => ({})); throw new Error(error.message || 'API request failed'); } @@ -594,7 +594,7 @@ class AdminAPI { }); if (!response.ok) { - const error = await response.json(); + const error = await response.json().catch(() => ({})); throw new Error(error.error || 'Failed to update user role'); } @@ -691,7 +691,7 @@ class AdminAPI { }); if (!response.ok) { - const error = await response.json(); + const error = await response.json().catch(() => ({})); throw new Error(error.error || 'Failed to update retention policy'); } @@ -720,7 +720,7 @@ class AdminAPI { }); if (!response.ok) { - const error = await response.json(); + const error = await response.json().catch(() => ({})); throw new Error(error.error || 'Failed to update entitlements'); } @@ -795,7 +795,7 @@ class AdminAPI { }); if (!response.ok) { - const error = await response.json(); + const error = await response.json().catch(() => ({})); throw new Error(error.error || 'Failed to update settings'); } diff --git a/packages/frontend/src/lib/api/auth.ts b/packages/frontend/src/lib/api/auth.ts index bdb075fa..3a016b41 100644 --- a/packages/frontend/src/lib/api/auth.ts +++ b/packages/frontend/src/lib/api/auth.ts @@ -63,7 +63,9 @@ export class AuthAPI { }); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Registration failed'); } @@ -80,7 +82,9 @@ export class AuthAPI { }); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Login failed'); } @@ -98,7 +102,9 @@ export class AuthAPI { }); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Logout failed'); } } @@ -114,7 +120,9 @@ export class AuthAPI { }); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to get user info'); } @@ -145,7 +153,9 @@ export class AuthAPI { const response = await fetch(`${getApiBaseUrl()}/auth/providers`); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to get auth providers'); } @@ -166,7 +176,9 @@ export class AuthAPI { ); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to get authorization URL'); } @@ -187,7 +199,9 @@ export class AuthAPI { }); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Login failed'); } @@ -202,7 +216,9 @@ export class AuthAPI { }); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to get identities'); } @@ -218,7 +234,9 @@ export class AuthAPI { }); if (!response.ok) { - const error: ErrorResponse = await response.json(); + const error: ErrorResponse = await response + .json() + .catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to unlink identity'); } } diff --git a/packages/frontend/src/lib/api/exceptions.ts b/packages/frontend/src/lib/api/exceptions.ts index 9f605d15..b1065380 100644 --- a/packages/frontend/src/lib/api/exceptions.ts +++ b/packages/frontend/src/lib/api/exceptions.ts @@ -50,8 +50,8 @@ export async function getExceptionByLogId( } if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to get exception'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to get exception (HTTP ${response.status})`); } return response.json(); @@ -75,8 +75,8 @@ export async function getExceptionById( } if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to get exception'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to get exception (HTTP ${response.status})`); } return response.json(); @@ -116,8 +116,8 @@ export async function getErrorGroups( }); if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to get error groups'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to get error groups (HTTP ${response.status})`); } return response.json(); @@ -147,8 +147,8 @@ export async function getTopErrorGroups(params: { }); if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to get top error groups'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to get top error groups (HTTP ${response.status})`); } return response.json(); @@ -172,8 +172,8 @@ export async function getErrorGroupById( } if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to get error group'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to get error group (HTTP ${response.status})`); } return response.json(); @@ -196,8 +196,8 @@ export async function updateErrorGroupStatus( }); if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to update error group status'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to update error group status (HTTP ${response.status})`); } return response.json(); @@ -231,8 +231,8 @@ export async function getErrorGroupTrend(params: { ); if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to get error group trend'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to get error group trend (HTTP ${response.status})`); } return response.json(); @@ -266,8 +266,8 @@ export async function getErrorGroupLogs(params: { ); if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to get error group logs'); + const error = await response.json().catch(() => ({})); + throw new Error(error.error || `Failed to get error group logs (HTTP ${response.status})`); } return response.json(); diff --git a/packages/frontend/src/lib/stores/auth.ts b/packages/frontend/src/lib/stores/auth.ts index 88fa168c..e4772aef 100644 --- a/packages/frontend/src/lib/stores/auth.ts +++ b/packages/frontend/src/lib/stores/auth.ts @@ -19,17 +19,40 @@ export interface AuthState { const STORAGE_KEY = 'logtide_auth'; +function isValidStoredAuth(data: unknown): data is { user: User; token: string } { + if (typeof data !== 'object' || data === null) { + return false; + } + const candidate = data as { user?: unknown; token?: unknown }; + if (typeof candidate.token !== 'string') { + return false; + } + const user = candidate.user; + if (typeof user !== 'object' || user === null) { + return false; + } + const u = user as { id?: unknown; email?: unknown; name?: unknown }; + return ( + typeof u.id === 'string' && + typeof u.email === 'string' && + typeof u.name === 'string' + ); +} + function loadInitialState(): AuthState { if (browser) { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { const data = JSON.parse(stored); - return { - user: data.user, - token: data.token, - loading: false, - }; + if (isValidStoredAuth(data)) { + return { + user: data.user, + token: data.token, + loading: false, + }; + } + localStorage.removeItem(STORAGE_KEY); } } catch (e) { console.error('Failed to load auth state:', e); diff --git a/packages/frontend/src/lib/stores/custom-dashboards.ts b/packages/frontend/src/lib/stores/custom-dashboards.ts index 1c0ee28d..1d8cf0c6 100644 --- a/packages/frontend/src/lib/stores/custom-dashboards.ts +++ b/packages/frontend/src/lib/stores/custom-dashboards.ts @@ -64,6 +64,10 @@ const initialState: DashboardStoreState = { function createDashboardStore() { const { subscribe, set, update } = writable(initialState); + // Monotonic guard so a stale in-flight fetch cannot write into a + // dashboard that has since been switched away from. + let panelFetchSeq = 0; + function getState(): DashboardStoreState { return get({ subscribe }); } @@ -175,6 +179,9 @@ function createDashboardStore() { const dashboard = state.activeDashboard; if (!dashboard || dashboard.panels.length === 0) return; + const fetchSeq = ++panelFetchSeq; + const fetchedDashboardId = dashboard.id; + // Mark all panels as loading update((s) => { const next: Record = { ...s.panelData }; @@ -196,6 +203,11 @@ function createDashboardStore() { ); const now = Date.now(); update((s) => { + // Ignore the response if a newer fetch started or the active + // dashboard changed while this request was in flight. + if (fetchSeq !== panelFetchSeq || s.activeDashboard?.id !== fetchedDashboardId) { + return s; + } const next: Record = { ...s.panelData }; for (const [panelId, entry] of Object.entries(result.panels)) { next[panelId] = { @@ -210,6 +222,9 @@ function createDashboardStore() { } catch (e) { const message = e instanceof Error ? e.message : 'Failed to load panel data'; update((s) => { + if (fetchSeq !== panelFetchSeq || s.activeDashboard?.id !== fetchedDashboardId) { + return s; + } const next: Record = { ...s.panelData }; for (const p of dashboard.panels) { next[p.id] = { diff --git a/packages/frontend/src/lib/stores/monitoring.ts b/packages/frontend/src/lib/stores/monitoring.ts index e1f4f6ff..7da77195 100644 --- a/packages/frontend/src/lib/stores/monitoring.ts +++ b/packages/frontend/src/lib/stores/monitoring.ts @@ -39,15 +39,21 @@ const initialState: MonitoringState = { function createMonitoringStore() { const { subscribe, set, update } = writable(initialState); + let loadSeq = 0; + let detailSeq = 0; + return { subscribe, async load(organizationId: string, projectId?: string): Promise { + const seq = ++loadSeq; update((s) => ({ ...s, loading: true, error: null })); try { const { monitors } = await listMonitors(organizationId, projectId); + if (seq !== loadSeq) return; update((s) => ({ ...s, monitors, loading: false })); } catch (err) { + if (seq !== loadSeq) return; update((s) => ({ ...s, loading: false, @@ -57,6 +63,7 @@ function createMonitoringStore() { }, async loadDetail(id: string, organizationId: string): Promise { + const seq = ++detailSeq; update((s) => ({ ...s, detailLoading: true, detailError: null })); try { const [monitorRes, resultsRes, uptimeRes] = await Promise.all([ @@ -64,6 +71,7 @@ function createMonitoringStore() { getMonitorResults(id, organizationId, 100), getMonitorUptime(id, organizationId, 90), ]); + if (seq !== detailSeq) return; update((s) => ({ ...s, selectedMonitor: monitorRes.monitor, @@ -72,6 +80,7 @@ function createMonitoringStore() { detailLoading: false, })); } catch (err) { + if (seq !== detailSeq) return; update((s) => ({ ...s, detailLoading: false, diff --git a/packages/frontend/src/lib/stores/notification-channels.ts b/packages/frontend/src/lib/stores/notification-channels.ts index 80c2837c..2cd91cca 100644 --- a/packages/frontend/src/lib/stores/notification-channels.ts +++ b/packages/frontend/src/lib/stores/notification-channels.ts @@ -109,9 +109,10 @@ function createNotificationChannelsStore() { try { const defaults = await notificationChannelsAPI.getDefaults(organizationId); - update((s) => ({ ...s, defaults, defaultsLoading: false })); + update((s) => ({ ...s, defaults, defaultsLoading: false, error: null })); } catch (error) { - update((s) => ({ ...s, defaultsLoading: false })); + const errorMessage = error instanceof Error ? error.message : 'Failed to load defaults'; + update((s) => ({ ...s, defaultsLoading: false, error: errorMessage })); } }, diff --git a/packages/frontend/src/lib/utils/siem.ts b/packages/frontend/src/lib/utils/siem.ts index 0ffd8f95..f6a2127f 100644 --- a/packages/frontend/src/lib/utils/siem.ts +++ b/packages/frontend/src/lib/utils/siem.ts @@ -15,7 +15,7 @@ export function getStatusLabel(status: string): string { export function formatDate(dateStr: string): string { const date = new Date(dateStr); - return date.toLocaleDateString(undefined, { + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', @@ -26,7 +26,7 @@ export function formatDate(dateStr: string): string { export function formatShortDate(dateStr: string): string { const date = new Date(dateStr); - return date.toLocaleDateString(undefined, { + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', From a2c6d092a4c52c722bc7867e3f1111cc769b8e3b Mon Sep 17 00:00:00 2001 From: Polliog Date: Sat, 20 Jun 2026 21:34:03 +0200 Subject: [PATCH 03/10] fix frontend component bugs from bug hunt --- .../src/lib/components/AppLayout.svelte | 12 ++++-- .../lib/components/CreateApiKeyDialog.svelte | 5 ++- .../src/lib/components/SigmaSyncDialog.svelte | 7 ++-- .../lib/components/alerts/AlertPreview.svelte | 9 ++++- .../DashboardContainer.svelte | 16 +++++--- .../exceptions/ExceptionDetailsDialog.svelte | 11 +++++- .../notification-channels/ChannelsList.svelte | 17 ++------- .../CreateChannelDialog.svelte | 37 +++++++++++++++---- .../siem/dashboard/MitreHeatmap.svelte | 29 +++++++++------ .../siem/dashboard/SeverityPieChart.svelte | 6 ++- .../lib/components/ui/sonner/sonner.svelte | 4 +- 11 files changed, 100 insertions(+), 53 deletions(-) diff --git a/packages/frontend/src/lib/components/AppLayout.svelte b/packages/frontend/src/lib/components/AppLayout.svelte index 5ce5e5af..8fbf43b0 100644 --- a/packages/frontend/src/lib/components/AppLayout.svelte +++ b/packages/frontend/src/lib/components/AppLayout.svelte @@ -296,15 +296,21 @@ shortcutsStore.install(); // First-time hint toast + let hintTimer: ReturnType | null = null; if (!shortcutsStore.hasShownHint()) { const mod = getPlatform().modSymbol; - setTimeout(() => { + // Mark as shown immediately so a fast unmount before the toast fires + // does not cause the hint to reappear on the next mount. + shortcutsStore.markHintShown(); + hintTimer = setTimeout(() => { toastStore.info(`Pro tip: Press ${mod}+K to open command palette, or ? for shortcuts`, 8000); - shortcutsStore.markHintShown(); }, 3000); } - return () => shortcutsStore.uninstall(); + return () => { + if (hintTimer) clearTimeout(hintTimer); + shortcutsStore.uninstall(); + }; }); diff --git a/packages/frontend/src/lib/components/CreateApiKeyDialog.svelte b/packages/frontend/src/lib/components/CreateApiKeyDialog.svelte index 0fec9e51..d4874a41 100644 --- a/packages/frontend/src/lib/components/CreateApiKeyDialog.svelte +++ b/packages/frontend/src/lib/components/CreateApiKeyDialog.svelte @@ -37,8 +37,9 @@ let dsn = $derived.by(() => { if (!generatedApiKey) return ''; - const host = apiUrlValue.replace('https://', '').replace('http://', ''); - return `https://${generatedApiKey}@${host}`; + const scheme = apiUrlValue.startsWith('http://') ? 'http' : 'https'; + const host = apiUrlValue.replace(/^https?:\/\//, ''); + return `${scheme}://${generatedApiKey}@${host}`; }); function parseOrigins(raw: string): string[] | null { diff --git a/packages/frontend/src/lib/components/SigmaSyncDialog.svelte b/packages/frontend/src/lib/components/SigmaSyncDialog.svelte index aeb5f8a6..0d4041b6 100644 --- a/packages/frontend/src/lib/components/SigmaSyncDialog.svelte +++ b/packages/frontend/src/lib/components/SigmaSyncDialog.svelte @@ -188,10 +188,9 @@

Commit: {syncResult.commitHash.substring( - 0, - 7, - )}{syncResult.commitHash + ? syncResult.commitHash.substring(0, 7) + : "-"}

diff --git a/packages/frontend/src/lib/components/alerts/AlertPreview.svelte b/packages/frontend/src/lib/components/alerts/AlertPreview.svelte index cca52a64..14d11e90 100644 --- a/packages/frontend/src/lib/components/alerts/AlertPreview.svelte +++ b/packages/frontend/src/lib/components/alerts/AlertPreview.svelte @@ -56,7 +56,10 @@ const timeRangeOptions: PreviewRange[] = ["1d", "7d", "14d", "30d"]; + let loadSeq = 0; + async function loadPreview() { + const requestId = ++loadSeq; loading = true; error = null; @@ -71,12 +74,16 @@ previewRange: timeRange, }); + if (requestId !== loadSeq) return; data = response.preview; } catch (e) { + if (requestId !== loadSeq) return; error = e instanceof Error ? e.message : "Failed to load preview"; toastStore.error(error); } finally { - loading = false; + if (requestId === loadSeq) { + loading = false; + } } } diff --git a/packages/frontend/src/lib/components/custom-dashboards/DashboardContainer.svelte b/packages/frontend/src/lib/components/custom-dashboards/DashboardContainer.svelte index cff4022a..3bf56a59 100644 --- a/packages/frontend/src/lib/components/custom-dashboards/DashboardContainer.svelte +++ b/packages/frontend/src/lib/components/custom-dashboards/DashboardContainer.svelte @@ -131,11 +131,17 @@ const colW = colWidthPx(); const rowStride = ROW_HEIGHT_PX + ROW_GAP_PX; - // Convert pixel delta into "stored" 12-col units regardless of viewport. - // This way resizing on a tablet (6 visible cols) still updates the - // logical width in the canonical 12-col reference space. - const visibleDeltaCols = Math.round(dx / (colW + COL_GAP_PX)); - const storedDeltaCols = Math.round((visibleDeltaCols / effectiveCols) * 12); + // Width resize only makes sense on the canonical 12-col desktop grid. + // Below 12 effective columns the visible->stored conversion snaps the + // logical width by whole rows (a single visible column maps to 12/effectiveCols + // stored units), which makes resizing erratic and unusable on tablet/mobile. + // In those breakpoints we keep the stored width unchanged and allow only + // height resizing. + let storedDeltaCols = 0; + if (effectiveCols >= 12) { + const visibleDeltaCols = Math.round(dx / (colW + COL_GAP_PX)); + storedDeltaCols = visibleDeltaCols; + } const deltaRows = Math.round(dy / rowStride); const newW = Math.min(12, Math.max(resizeState.minW, resizeState.startW + storedDeltaCols)); diff --git a/packages/frontend/src/lib/components/exceptions/ExceptionDetailsDialog.svelte b/packages/frontend/src/lib/components/exceptions/ExceptionDetailsDialog.svelte index e14ba261..db519286 100644 --- a/packages/frontend/src/lib/components/exceptions/ExceptionDetailsDialog.svelte +++ b/packages/frontend/src/lib/components/exceptions/ExceptionDetailsDialog.svelte @@ -93,8 +93,15 @@ function viewErrorGroup() { if (exception) { - // Navigate to error group page - goto(`/dashboard/errors?fingerprint=${exception.exception.fingerprint}&organizationId=${organizationId}`); + // Navigate to the error groups list filtered to this exception. + // The list page reads the `search` param (ILIKE on exception type/message); + // it does not read a `fingerprint` param, so use the exception type as the + // search term to land on the matching group. + const params = new URLSearchParams({ + organizationId, + search: exception.exception.exceptionType, + }); + goto(`/dashboard/errors?${params.toString()}`); onClose(); } } diff --git a/packages/frontend/src/lib/components/notification-channels/ChannelsList.svelte b/packages/frontend/src/lib/components/notification-channels/ChannelsList.svelte index b0533fff..88614f12 100644 --- a/packages/frontend/src/lib/components/notification-channels/ChannelsList.svelte +++ b/packages/frontend/src/lib/components/notification-channels/ChannelsList.svelte @@ -42,18 +42,9 @@ import TestTube from '@lucide/svelte/icons/test-tube'; import Bell from '@lucide/svelte/icons/bell'; - let channels = $state([]); - let loading = $state(false); - - let currentOrg = $state<{ id: string } | null>(null); - organizationStore.subscribe((state) => { - currentOrg = state.currentOrganization; - }); - - notificationChannelsStore.subscribe((state) => { - channels = state.channels; - loading = state.loading; - }); + let currentOrg = $derived($organizationStore.currentOrganization); + let channels = $derived($notificationChannelsStore.channels); + let loading = $derived($notificationChannelsStore.loading); // Load channels when org changes $effect(() => { @@ -119,7 +110,7 @@ } function formatDate(dateStr: string): string { - return new Date(dateStr).toLocaleDateString(); + return new Date(dateStr).toLocaleDateString('en-US'); } function getConfigSummary(channel: NotificationChannel): string { diff --git a/packages/frontend/src/lib/components/notification-channels/CreateChannelDialog.svelte b/packages/frontend/src/lib/components/notification-channels/CreateChannelDialog.svelte index fe862a9f..701dc07d 100644 --- a/packages/frontend/src/lib/components/notification-channels/CreateChannelDialog.svelte +++ b/packages/frontend/src/lib/components/notification-channels/CreateChannelDialog.svelte @@ -59,6 +59,10 @@ let webhookAuthToken = $state(''); let webhookAuthUser = $state(''); let webhookAuthPass = $state(''); + // Whether the channel being edited already has a stored secret. Secrets are + // never sent back to the client, so on edit we keep the secret fields empty + // and only submit a new value when the user types one. + let hasStoredAuthSecret = $state(false); const isEditing = $derived(!!channel); @@ -74,6 +78,7 @@ webhookAuthToken = ''; webhookAuthUser = ''; webhookAuthPass = ''; + hasStoredAuthSecret = false; testResult = null; } @@ -90,15 +95,21 @@ webhookUrl = config.url; webhookMethod = config.method === 'PUT' ? 'PUT' : 'POST'; webhookHeaders = config.headers ? JSON.stringify(config.headers, null, 2) : ''; + // Never re-hydrate stored secrets into the DOM. Track that a secret + // exists so we can show a placeholder, and leave the secret fields + // empty; a blank value on submit means "keep the existing secret". if (config.auth?.type === 'bearer') { webhookAuthType = 'bearer'; - webhookAuthToken = config.auth.token; + webhookAuthToken = ''; + hasStoredAuthSecret = true; } else if (config.auth?.type === 'basic') { webhookAuthType = 'basic'; webhookAuthUser = config.auth.username; - webhookAuthPass = config.auth.password; + webhookAuthPass = ''; + hasStoredAuthSecret = true; } else { webhookAuthType = 'none'; + hasStoredAuthSecret = false; } } } @@ -132,14 +143,24 @@ } } - if (webhookAuthType === 'bearer' && webhookAuthToken) { - config.auth = { type: 'bearer', token: webhookAuthToken }; + if (webhookAuthType === 'bearer') { + // Only send a token when the user typed a new one. On edit, a + // blank field means "keep the existing secret" (the stored secret + // is never sent to the client), so we omit auth and let the + // backend preserve it. + if (webhookAuthToken) { + config.auth = { type: 'bearer', token: webhookAuthToken }; + } } else if (webhookAuthType === 'basic' && webhookAuthUser) { - config.auth = { + const basicAuth: { type: 'basic'; username: string; password?: string } = { type: 'basic', username: webhookAuthUser, - password: webhookAuthPass, }; + // Same rule for the password: only send a new one when typed. + if (webhookAuthPass) { + basicAuth.password = webhookAuthPass; + } + config.auth = basicAuth; } return config; @@ -412,7 +433,7 @@ @@ -436,7 +457,7 @@ diff --git a/packages/frontend/src/lib/components/siem/dashboard/MitreHeatmap.svelte b/packages/frontend/src/lib/components/siem/dashboard/MitreHeatmap.svelte index 7ea2a848..e5f6ce3a 100644 --- a/packages/frontend/src/lib/components/siem/dashboard/MitreHeatmap.svelte +++ b/packages/frontend/src/lib/components/siem/dashboard/MitreHeatmap.svelte @@ -1,5 +1,4 @@ Date: Sat, 20 Jun 2026 21:34:04 +0200 Subject: [PATCH 04/10] fix frontend route bugs from bug hunt --- .../src/routes/auth/callback/+page.svelte | 7 +++ .../routes/dashboard/admin/+layout@.svelte | 52 ++++++++++++++++++- .../admin/organizations/[id]/+page.svelte | 38 +++++++++++++- .../admin/system-health/+page.svelte | 7 ++- .../routes/dashboard/admin/usage/+page.svelte | 46 +++++++++++++--- .../dashboard/admin/users/[id]/+page.svelte | 36 ++++++++++++- .../src/routes/dashboard/alerts/+page.svelte | 51 +++++++++++++----- .../src/routes/dashboard/errors/+page.svelte | 36 +++++++++++-- .../routes/dashboard/errors/[id]/+page.svelte | 15 +++--- .../src/routes/dashboard/metrics/+page.svelte | 5 +- .../routes/dashboard/monitoring/+page.svelte | 45 ++++++++++++---- .../dashboard/monitoring/[id]/+page.svelte | 26 +++++++++- .../projects/[id]/settings/+page.svelte | 6 +-- .../src/routes/dashboard/search/+page.svelte | 45 ++++++++++++++-- .../dashboard/security/incidents/+page.svelte | 36 ++++++++++--- .../dashboard/settings/audit-log/+page.svelte | 2 +- .../dashboard/settings/general/+page.svelte | 1 + .../dashboard/settings/members/+page.svelte | 2 +- .../settings/pii-masking/+page.svelte | 12 +++++ .../src/routes/dashboard/traces/+page.svelte | 39 ++++++++++---- .../routes/dashboard/traces/[id]/+page.svelte | 22 +++++--- .../frontend/src/routes/login/+page.svelte | 11 +++- .../[orgSlug]/[projectSlug]/+page.svelte | 38 +++++++++----- 23 files changed, 483 insertions(+), 95 deletions(-) diff --git a/packages/frontend/src/routes/auth/callback/+page.svelte b/packages/frontend/src/routes/auth/callback/+page.svelte index 0e4d33ad..e2a4cefd 100644 --- a/packages/frontend/src/routes/auth/callback/+page.svelte +++ b/packages/frontend/src/routes/auth/callback/+page.svelte @@ -24,6 +24,13 @@ const expires = page.url.searchParams.get('expires'); const isNewUser = page.url.searchParams.get('new_user') === 'true'; + // Scrub the token (and other sensitive params) from the URL/history + // immediately so it never leaks into browser history, the referrer, or + // any pageview logging that captures window.location.href. + if (typeof history !== 'undefined') { + history.replaceState(null, '', '/auth/callback'); + } + if (!token) { error = 'No authentication token received. Please try logging in again.'; loading = false; diff --git a/packages/frontend/src/routes/dashboard/admin/+layout@.svelte b/packages/frontend/src/routes/dashboard/admin/+layout@.svelte index 81559af8..eba8c83d 100644 --- a/packages/frontend/src/routes/dashboard/admin/+layout@.svelte +++ b/packages/frontend/src/routes/dashboard/admin/+layout@.svelte @@ -15,9 +15,57 @@ import { cn } from "$lib/utils"; import Footer from "$lib/components/Footer.svelte"; import type { Snippet } from "svelte"; + import { authStore } from "$lib/stores/auth"; + import { UsersAPI } from "$lib/api/users"; + import { goto } from "$app/navigation"; + import { browser } from "$app/environment"; + import { untrack } from "svelte"; + import { get } from "svelte/store"; let { children }: { children: Snippet } = $props(); + // Centralized admin guard for the whole /dashboard/admin section. + // This layout resets the layout chain (the trailing "@"), so it does not + // inherit the dashboard auth guard; enforce authentication + is_admin here + // so individual admin pages cannot accidentally omit the check. + let adminResolved = $state(false); + + const usersAPI = new UsersAPI(() => get(authStore).token); + + $effect(() => { + if (!browser) return; + + if (!$authStore.token) { + untrack(() => goto("/login")); + return; + } + + if (!$authStore.user) return; + + if ($authStore.user.is_admin === undefined) { + untrack(() => { + usersAPI + .getCurrentUser() + .then(({ user }) => { + const currentUser = get(authStore).user; + if (currentUser) { + authStore.updateUser({ ...currentUser, ...user }); + } + if (user.is_admin) { + adminResolved = true; + } else { + goto("/dashboard"); + } + }) + .catch(() => goto("/dashboard")); + }); + } else if ($authStore.user.is_admin === false) { + untrack(() => goto("/dashboard")); + } else { + adminResolved = true; + } + }); + const navigation = [ { name: "Dashboard", @@ -215,7 +263,9 @@
- {@render children()} + {#if adminResolved} + {@render children()} + {/if}