From 61c5937a94aa0acbcbb6b2034ffabc6facf38139 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Fri, 6 Mar 2026 01:00:09 +0100 Subject: [PATCH] better reconnect logic and added bing verification --- .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- .../components/DeviceBlockSection.tsx | 47 +++-- .../components/MaintenanceSection.tsx | 82 +++----- .../components/SessionExportSection.tsx | 10 +- .../components/SessionReportsSection.tsx | 32 +-- .../components/TenantAdminSection.tsx | 68 +++--- .../components/TenantManagementSection.tsx | 45 ++-- .../app/admin-configuration/page.tsx | 65 +++--- .../app/analyze-rules/page.tsx | 46 +---- .../autopilot-monitor-web/app/audit/page.tsx | 21 +- .../app/dashboard/page.tsx | 113 ++++------ .../app/diagnosis/[sessionId]/page.tsx | 47 +++-- .../app/fleet-health/page.tsx | 33 ++- .../app/gather-rules/page.tsx | 46 +---- .../app/geographic-performance/page.tsx | 13 +- .../app/health-check/page.tsx | 21 +- .../app/ime-log-patterns/page.tsx | 30 ++- .../app/platform-metrics/page.tsx | 19 +- .../app/platform-usage-metrics/page.tsx | 19 +- .../app/progress/page.tsx | 34 +-- .../app/sessions/[sessionId]/page.tsx | 104 ++++------ .../app/settings/page.tsx | 195 ++++++++---------- .../app/usage-metrics/page.tsx | 21 +- .../contexts/AuthContext.tsx | 5 +- .../contexts/SignalRContext.tsx | 55 ++--- .../lib/authenticatedFetch.ts | 52 +++++ .../public/BingSiteAuth.xml | 4 + .../tsconfig.tsbuildinfo | 2 +- 28 files changed, 521 insertions(+), 710 deletions(-) create mode 100644 src/Web/autopilot-monitor-web/lib/authenticatedFetch.ts create mode 100644 src/Web/autopilot-monitor-web/public/BingSiteAuth.xml diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index bfd03aba..77f83230 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -242 +243 diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/components/DeviceBlockSection.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/components/DeviceBlockSection.tsx index f1187ea2..66b91356 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/components/DeviceBlockSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/components/DeviceBlockSection.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { TenantConfiguration } from "./TenantManagementSection"; import { TenantSearchSelect } from "./TenantSearchSelect"; @@ -60,15 +61,14 @@ export function DeviceBlockSection({ if (!tenantId) return; try { setLoadingBlockedDevices(true); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const response = await fetch(`${API_BASE_URL}/api/devices/blocked?tenantId=${encodeURIComponent(tenantId)}`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/devices/blocked?tenantId=${encodeURIComponent(tenantId)}`, getAccessToken); if (!response.ok) throw new Error(`Failed to load blocked devices: ${response.statusText}`); const data = await response.json(); setBlockedDevices(data.blocked ?? []); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while loading blocked devices"); + } setError(err instanceof Error ? err.message : "Failed to load blocked devices"); } finally { setLoadingBlockedDevices(false); @@ -78,16 +78,15 @@ export function DeviceBlockSection({ const fetchAllBlockedDevices = async () => { try { setLoadingAllBlocked(true); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const response = await fetch(`${API_BASE_URL}/api/galactic/devices/blocked`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/galactic/devices/blocked`, getAccessToken); if (!response.ok) throw new Error(`Failed to load blocked devices: ${response.statusText}`); const data = await response.json(); setBlockedDevices(data.blocked ?? []); setBlockListTenantId(""); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while loading all blocked devices"); + } setError(err instanceof Error ? err.message : "Failed to load all blocked devices"); } finally { setLoadingAllBlocked(false); @@ -104,11 +103,9 @@ export function DeviceBlockSection({ try { setBlockingDevice(true); setError(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const response = await fetch(`${API_BASE_URL}/api/devices/block`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/devices/block`, getAccessToken, { method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tenantId: blockTenantId, serialNumber: blockSerialNumber.trim(), @@ -130,6 +127,9 @@ export function DeviceBlockSection({ setBlockAction("Block"); if (blockListTenantId === blockTenantId) await fetchBlockedDevices(blockTenantId); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while blocking device"); + } setError(err instanceof Error ? err.message : "Failed to block device"); } finally { setBlockingDevice(false); @@ -140,11 +140,10 @@ export function DeviceBlockSection({ try { setUnblockingDevice(serialNumber); setError(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/devices/block/${encodeURIComponent(serialNumber)}?tenantId=${encodeURIComponent(tenantId)}`, - { method: "DELETE", headers: { Authorization: `Bearer ${token}` } } + getAccessToken, + { method: "DELETE" } ); const result = await response.json(); if (!response.ok) throw new Error(result.message || response.statusText); @@ -152,6 +151,9 @@ export function DeviceBlockSection({ setTimeout(() => setSuccessMessage(null), 3000); setBlockedDevices((prev) => prev.filter((d) => d.serialNumber !== serialNumber || d.tenantId !== tenantId)); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while unblocking device"); + } setError(err instanceof Error ? err.message : "Failed to unblock device"); } finally { setUnblockingDevice(null); @@ -166,12 +168,10 @@ export function DeviceBlockSection({ try { setKillingDevice(device.serialNumber); setError(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); const remainingHours = Math.max(1, Math.ceil((new Date(device.unblockAt).getTime() - Date.now()) / 3600000)); - const response = await fetch(`${API_BASE_URL}/api/devices/block`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/devices/block`, getAccessToken, { method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tenantId: device.tenantId, serialNumber: device.serialNumber, @@ -186,6 +186,9 @@ export function DeviceBlockSection({ setTimeout(() => setSuccessMessage(null), 4000); await fetchBlockedDevices(device.tenantId); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while sending kill signal"); + } setError(err instanceof Error ? err.message : "Failed to send kill signal"); } finally { setKillingDevice(null); diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/components/MaintenanceSection.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/components/MaintenanceSection.tsx index aa8e14d7..1d9c9e21 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/components/MaintenanceSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/components/MaintenanceSection.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface MaintenanceSectionProps { getAccessToken: () => Promise; @@ -27,31 +28,26 @@ export function MaintenanceSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - const queryParams = maintenanceDate ? `?date=${maintenanceDate}` : ''; - const response = await fetch(`${API_BASE_URL}/api/maintenance/trigger${queryParams}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/maintenance/trigger${queryParams}`, getAccessToken, { method: "POST", - headers: { - 'Authorization': `Bearer ${token}` - } }); if (!response.ok) { throw new Error(`Failed to trigger maintenance: ${response.statusText}`); } - const result = await response.json(); const dateInfo = maintenanceDate ? ` for ${maintenanceDate}` : ''; setSuccessMessage(`Maintenance job completed successfully${dateInfo}!`); // Auto-hide success message after 5 seconds setTimeout(() => setSuccessMessage(null), 5000); } catch (err) { - console.error("Error triggering maintenance:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while triggering maintenance"); + } else { + console.error("Error triggering maintenance:", err); + } setError(err instanceof Error ? err.message : "Failed to trigger maintenance job"); } finally { setTriggeringMaintenance(false); @@ -64,16 +60,8 @@ export function MaintenanceSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=analyze`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=analyze`, getAccessToken, { method: "POST", - headers: { - 'Authorization': `Bearer ${token}` - } }); if (!response.ok) { @@ -85,7 +73,11 @@ export function MaintenanceSection({ setSuccessMessage(`Analyze rules reseeded from GitHub: ${result.analyze?.deleted ?? 0} deleted, ${result.analyze?.written ?? 0} written`); setTimeout(() => setSuccessMessage(null), 5000); } catch (err) { - console.error("Error reseeding analyze rules:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while reseeding analyze rules"); + } else { + console.error("Error reseeding analyze rules:", err); + } setError(err instanceof Error ? err.message : "Failed to reseed analyze rules"); } finally { setReseedingRules(false); @@ -98,16 +90,8 @@ export function MaintenanceSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=gather`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=gather`, getAccessToken, { method: "POST", - headers: { - 'Authorization': `Bearer ${token}` - } }); if (!response.ok) { @@ -119,7 +103,11 @@ export function MaintenanceSection({ setSuccessMessage(`Gather rules reseeded from GitHub: ${result.gather?.deleted ?? 0} deleted, ${result.gather?.written ?? 0} written`); setTimeout(() => setSuccessMessage(null), 5000); } catch (err) { - console.error("Error reseeding gather rules:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while reseeding gather rules"); + } else { + console.error("Error reseeding gather rules:", err); + } setError(err instanceof Error ? err.message : "Failed to reseed gather rules"); } finally { setReseedingGatherRules(false); @@ -132,16 +120,8 @@ export function MaintenanceSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=ime`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=ime`, getAccessToken, { method: "POST", - headers: { - 'Authorization': `Bearer ${token}` - } }); if (!response.ok) { @@ -153,7 +133,11 @@ export function MaintenanceSection({ setSuccessMessage(`IME log patterns reseeded from GitHub: ${result.ime?.deleted ?? 0} deleted, ${result.ime?.written ?? 0} written`); setTimeout(() => setSuccessMessage(null), 5000); } catch (err) { - console.error("Error reseeding IME log patterns:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while reseeding IME log patterns"); + } else { + console.error("Error reseeding IME log patterns:", err); + } setError(err instanceof Error ? err.message : "Failed to reseed IME log patterns"); } finally { setReseedingImePatterns(false); @@ -166,16 +150,8 @@ export function MaintenanceSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=all`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/reseed-from-github?type=all`, getAccessToken, { method: "POST", - headers: { - 'Authorization': `Bearer ${token}` - } }); if (!response.ok) { @@ -191,7 +167,11 @@ export function MaintenanceSection({ ); setTimeout(() => setSuccessMessage(null), 8000); } catch (err) { - console.error("Error reseeding from GitHub:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while reseeding from GitHub"); + } else { + console.error("Error reseeding from GitHub:", err); + } setError(err instanceof Error ? err.message : "Failed to reseed from GitHub"); } finally { setFetchingFromGitHub(false); diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionExportSection.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionExportSection.tsx index d02d9942..b97b0edc 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionExportSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionExportSection.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { TenantConfiguration } from "./TenantManagementSection"; import { TenantSearchSelect } from "./TenantSearchSelect"; import { SessionExportEvent, generateCsvExport, generateUiExport } from "@/lib/sessionExportUtils"; @@ -44,17 +45,18 @@ export function SessionExportSection({ setExportLoading(true); setExportError(null); setExportedEvents(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const res = await fetch( + const res = await authenticatedFetch( `${API_BASE_URL}/api/sessions/${sid}/events?tenantId=${encodeURIComponent(tid)}`, - { headers: { Authorization: `Bearer ${token}` } } + getAccessToken ); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); const data = await res.json(); if (!data.success) throw new Error(data.message || "Backend returned error"); setExportedEvents(data.events ?? []); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while fetching export events"); + } setExportError(err instanceof Error ? err.message : "Failed to fetch events"); } finally { setExportLoading(false); diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionReportsSection.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionReportsSection.tsx index 3a73c6b6..6b163db1 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionReportsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/components/SessionReportsSection.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface SessionReport { reportId: string; @@ -77,12 +78,10 @@ export function SessionReportsSection({ const handleDownload = async (blobName: string) => { try { setDownloadingBlob(blobName); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const res = await fetch( + const res = await authenticatedFetch( `${API_BASE_URL}/api/galactic/session-reports/download-url?blobName=${encodeURIComponent(blobName)}`, - { headers: { Authorization: `Bearer ${token}` } } + getAccessToken ); if (!res.ok) throw new Error(`Failed to get download URL: ${res.statusText}`); const data = await res.json(); @@ -93,6 +92,9 @@ export function SessionReportsSection({ a.download = blobName; a.click(); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while downloading report"); + } setError(err instanceof Error ? err.message : "Failed to download report"); } finally { setDownloadingBlob(null); @@ -104,17 +106,13 @@ export function SessionReportsSection({ try { setSavingNote(true); setNoteSaveResult(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const res = await fetch( + const res = await authenticatedFetch( `${API_BASE_URL}/api/galactic/session-reports/${selectedReport.reportId}/note`, + getAccessToken, { method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ adminNote: adminNoteValue }), } ); @@ -126,6 +124,9 @@ export function SessionReportsSection({ setReports(prev => prev.map(r => r.reportId === selectedReport.reportId ? updated : r)); setNoteSaveResult("saved"); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while saving admin note"); + } setNoteSaveResult(err instanceof Error ? err.message : "Failed to save note"); } finally { setSavingNote(false); @@ -135,12 +136,8 @@ export function SessionReportsSection({ const fetchReports = async () => { try { setLoading(true); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const res = await fetch(`${API_BASE_URL}/api/galactic/session-reports`, { - headers: { Authorization: `Bearer ${token}` } - }); + const res = await authenticatedFetch(`${API_BASE_URL}/api/galactic/session-reports`, getAccessToken); if (res.status === 404) { // Table/container doesn't exist yet — no reports submitted so far @@ -151,6 +148,9 @@ export function SessionReportsSection({ const data = await res.json(); setReports(data.reports ?? []); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while loading reports"); + } setError(err instanceof Error ? err.message : "Failed to load reports"); } finally { setLoading(false); diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantAdminSection.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantAdminSection.tsx index 4541bc85..d1fb3e4a 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantAdminSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantAdminSection.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface TenantAdmin { tenantId: string; @@ -40,16 +41,7 @@ export function TenantAdminSection({ setLoadingAdmins(true); setCurrentAdminPage(0); // Reset to first page when loading new tenant - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/tenants/${tid}/admins`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/tenants/${tid}/admins`, getAccessToken); if (!response.ok) { throw new Error(`Failed to load admins: ${response.statusText}`); @@ -58,7 +50,11 @@ export function TenantAdminSection({ const data: TenantAdmin[] = await response.json(); setTenantAdmins(data); } catch (err) { - console.error("Error fetching tenant admins:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while fetching tenant admins"); + } else { + console.error("Error fetching tenant admins:", err); + } setError(err instanceof Error ? err.message : "Failed to load tenant admins"); } finally { setLoadingAdmins(false); @@ -77,17 +73,9 @@ export function TenantAdminSection({ setAddingAdmin(true); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/tenants/${tenantId}/admins`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/tenants/${tenantId}/admins`, getAccessToken, { method: "POST", - headers: { - "Content-Type": "application/json", - 'Authorization': `Bearer ${token}` - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ upn: newAdminEmail.trim() }), }); @@ -105,7 +93,11 @@ export function TenantAdminSection({ // Auto-hide success message after 3 seconds setTimeout(() => setSuccessMessage(null), 3000); } catch (err) { - console.error("Error adding tenant admin:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while adding tenant admin"); + } else { + console.error("Error adding tenant admin:", err); + } setError(err instanceof Error ? err.message : "Failed to add admin"); } finally { setAddingAdmin(false); @@ -121,16 +113,8 @@ export function TenantAdminSection({ setRemovingAdmin(adminUpn); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/tenants/${tenantId}/admins/${encodeURIComponent(adminUpn)}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/tenants/${tenantId}/admins/${encodeURIComponent(adminUpn)}`, getAccessToken, { method: "DELETE", - headers: { - 'Authorization': `Bearer ${token}` - }, }); if (!response.ok) { @@ -146,7 +130,11 @@ export function TenantAdminSection({ // Auto-hide success message after 3 seconds setTimeout(() => setSuccessMessage(null), 3000); } catch (err) { - console.error("Error removing tenant admin:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while removing tenant admin"); + } else { + console.error("Error removing tenant admin:", err); + } setError(err instanceof Error ? err.message : "Failed to remove admin"); } finally { setRemovingAdmin(null); @@ -158,17 +146,9 @@ export function TenantAdminSection({ setTogglingAdmin(adminUpn); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - const action = isEnabled ? 'disable' : 'enable'; - const response = await fetch(`${API_BASE_URL}/api/tenants/${tenantId}/admins/${encodeURIComponent(adminUpn)}/${action}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/tenants/${tenantId}/admins/${encodeURIComponent(adminUpn)}/${action}`, getAccessToken, { method: "PATCH", - headers: { - 'Authorization': `Bearer ${token}` - }, }); if (!response.ok) { @@ -189,7 +169,11 @@ export function TenantAdminSection({ // Auto-hide success message after 3 seconds setTimeout(() => setSuccessMessage(null), 3000); } catch (err) { - console.error("Error toggling tenant admin:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while toggling tenant admin"); + } else { + console.error("Error toggling tenant admin:", err); + } setError(err instanceof Error ? err.message : "Failed to toggle admin"); } finally { setTogglingAdmin(null); diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantManagementSection.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantManagementSection.tsx index 6aac2882..6e258c5f 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantManagementSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/components/TenantManagementSection.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { TenantAdminSection } from "./TenantAdminSection"; export interface TenantConfiguration { @@ -91,17 +92,9 @@ export function TenantManagementSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/config/${tenant.tenantId}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/config/${tenant.tenantId}`, getAccessToken, { method: "PUT", - headers: { - "Content-Type": "application/json", - 'Authorization': `Bearer ${token}` - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(tenant), }); @@ -119,7 +112,11 @@ export function TenantManagementSection({ // Auto-hide success message after 3 seconds setTimeout(() => setSuccessMessage(null), 3000); } catch (err) { - console.error("Error saving tenant configuration:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while saving tenant configuration"); + } else { + console.error("Error saving tenant configuration:", err); + } setError(err instanceof Error ? err.message : "Failed to save tenant configuration"); } finally { setSavingTenant(false); @@ -132,18 +129,10 @@ export function TenantManagementSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - const newValue = !tenant.allowInsecureAgentRequests; - const response = await fetch(`${API_BASE_URL}/api/config/${tenant.tenantId}/security-bypass`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/config/${tenant.tenantId}/security-bypass`, getAccessToken, { method: "PATCH", - headers: { - "Content-Type": "application/json", - 'Authorization': `Bearer ${token}` - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ allowInsecureAgentRequests: newValue }), }); @@ -169,6 +158,9 @@ export function TenantManagementSection({ ); setTimeout(() => setSuccessMessage(null), 4000); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while toggling security bypass"); + } setError(err instanceof Error ? err.message : "Failed to update security bypass"); } finally { setTogglingSecurityBypassTenant(null); @@ -181,16 +173,10 @@ export function TenantManagementSection({ setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - const isCurrentlyApproved = previewApproved.has(tenantId); - const response = await fetch(`${API_BASE_URL}/api/preview/whitelist/${tenantId}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/preview/whitelist/${tenantId}`, getAccessToken, { method: isCurrentlyApproved ? "DELETE" : "POST", - headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { @@ -215,6 +201,9 @@ export function TenantManagementSection({ ); setTimeout(() => setSuccessMessage(null), 4000); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while updating preview access"); + } setError(err instanceof Error ? err.message : "Failed to update preview access"); } finally { setTogglingPreviewTenant(null); diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx index d78d7a73..b6be1e57 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { ProtectedRoute } from "../../components/ProtectedRoute"; import { useAuth } from "../../contexts/AuthContext"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { TenantManagementSection, TenantConfiguration } from "./components/TenantManagementSection"; import { MaintenanceSection } from "./components/MaintenanceSection"; import { DiagnosticsLogPathsSection, DiagnosticsLogPath } from "./components/DiagnosticsLogPathsSection"; @@ -75,16 +76,7 @@ export default function AdminConfigurationPage() { setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/global/config`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/global/config`, getAccessToken); if (!response.ok) { throw new Error(`Failed to load admin configuration: ${response.statusText}`); @@ -103,7 +95,11 @@ export default function AdminConfigurationPage() { setGlobalDiagPaths([]); } } catch (err) { - console.error("Error fetching admin configuration:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while fetching admin configuration"); + } else { + console.error("Error fetching admin configuration:", err); + } setError(err instanceof Error ? err.message : "Failed to load admin configuration"); } finally { setLoadingConfig(false); @@ -119,18 +115,9 @@ export default function AdminConfigurationPage() { try { setLoadingTenants(true); - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - const [tenantsRes, previewRes] = await Promise.all([ - fetch(`${API_BASE_URL}/api/config/all`, { - headers: { 'Authorization': `Bearer ${token}` } - }), - fetch(`${API_BASE_URL}/api/preview/whitelist`, { - headers: { 'Authorization': `Bearer ${token}` } - }) + authenticatedFetch(`${API_BASE_URL}/api/config/all`, getAccessToken), + authenticatedFetch(`${API_BASE_URL}/api/preview/whitelist`, getAccessToken) ]); if (!tenantsRes.ok) { @@ -148,7 +135,11 @@ export default function AdminConfigurationPage() { setPreviewApproved(approvedIds); } } catch (err) { - console.error("Error fetching tenants:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while fetching tenants"); + } else { + console.error("Error fetching tenants:", err); + } setError(err instanceof Error ? err.message : "Failed to load tenants"); } finally { setLoadingTenants(false); @@ -166,17 +157,14 @@ export default function AdminConfigurationPage() { setError(null); setSuccessMessage(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const updatedConfig: AdminConfiguration = { ...adminConfig, diagnosticsGlobalLogPathsJson: JSON.stringify(paths), }; - const response = await fetch(`${API_BASE_URL}/api/global/config`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/global/config`, getAccessToken, { method: "PUT", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(updatedConfig), }); @@ -188,6 +176,9 @@ export default function AdminConfigurationPage() { setSuccessMessage("Global diagnostics log paths saved successfully!"); setTimeout(() => setSuccessMessage(null), 4000); } catch (err) { + if (err instanceof TokenExpiredError) { + console.error("Session expired while saving diagnostics paths"); + } setError(err instanceof Error ? err.message : "Failed to save diagnostics paths"); } finally { setSavingDiagPaths(false); @@ -211,17 +202,9 @@ export default function AdminConfigurationPage() { maintenanceBlockDurationHours: maintenanceBlockDurationHours, }; - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/global/config`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/global/config`, getAccessToken, { method: "PUT", - headers: { - "Content-Type": "application/json", - 'Authorization': `Bearer ${token}` - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(updatedConfig), }); @@ -236,7 +219,11 @@ export default function AdminConfigurationPage() { // Auto-hide success message after 3 seconds setTimeout(() => setSuccessMessage(null), 3000); } catch (err) { - console.error("Error saving admin configuration:", err); + if (err instanceof TokenExpiredError) { + console.error("Session expired while saving admin configuration"); + } else { + console.error("Error saving admin configuration:", err); + } setError(err instanceof Error ? err.message : "Failed to save admin configuration"); } finally { setSavingConfig(false); diff --git a/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx b/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx index 724a85d3..543ad112 100644 --- a/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx +++ b/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx @@ -7,6 +7,7 @@ import { ProtectedRoute } from "../../components/ProtectedRoute"; import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { downloadAsJson, stripInternalFields, bumpVersion } from "@/lib/rulePageHelpers"; import { StatCard } from "@/components/rules/StatCard"; import { RuleFilterBar } from "@/components/rules/RuleFilterBar"; @@ -226,16 +227,7 @@ export default function AnalyzeRulesPage() { setLoading(true); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/analyze`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/analyze`, getAccessToken); if (!response.ok) { throw new Error(`Failed to load analyze rules: ${response.statusText}`); @@ -265,16 +257,10 @@ export default function AnalyzeRulesPage() { setTogglingRuleId(rule.ruleId); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/analyze/${encodeURIComponent(rule.ruleId)}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/analyze/${encodeURIComponent(rule.ruleId)}`, getAccessToken, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify({ ...rule, enabled: !rule.enabled }), }); @@ -305,17 +291,9 @@ export default function AnalyzeRulesPage() { setDeletingRuleId(rule.ruleId); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - const url = `${API_BASE_URL}/api/rules/analyze/${encodeURIComponent(rule.ruleId)}`; - const response = await fetch(url, { + const response = await authenticatedFetch(url, getAccessToken, { method: "DELETE", - headers: { - Authorization: `Bearer ${token}`, - }, }); if (!response.ok) { @@ -345,11 +323,6 @@ export default function AnalyzeRulesPage() { setCreating(true); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - const payload = { ruleId: form.ruleId.trim(), title: form.title.trim(), @@ -366,11 +339,10 @@ export default function AnalyzeRulesPage() { relatedDocs: form.relatedDocs.filter(d => d.title.trim() && d.url.trim()), }; - const response = await fetch(`${API_BASE_URL}/api/rules/analyze`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/analyze`, getAccessToken, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), }); @@ -410,11 +382,6 @@ export default function AnalyzeRulesPage() { setSaving(true); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - const payload = { ...rule, title: form.title.trim(), @@ -434,11 +401,10 @@ export default function AnalyzeRulesPage() { }; const url = `${API_BASE_URL}/api/rules/analyze/${encodeURIComponent(rule.ruleId)}`; - const response = await fetch(url, { + const response = await authenticatedFetch(url, getAccessToken, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), }); diff --git a/src/Web/autopilot-monitor-web/app/audit/page.tsx b/src/Web/autopilot-monitor-web/app/audit/page.tsx index e5780055..19186180 100644 --- a/src/Web/autopilot-monitor-web/app/audit/page.tsx +++ b/src/Web/autopilot-monitor-web/app/audit/page.tsx @@ -7,6 +7,7 @@ import { useAuth } from '../../contexts/AuthContext'; import { useNotifications } from '../../contexts/NotificationContext'; import { ProtectedRoute } from '../../components/ProtectedRoute'; import { API_BASE_URL } from '@/lib/config'; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface AuditLogEntry { id: string; @@ -52,20 +53,10 @@ export default function AuditPage() { setLoading(true); } - const token = await getAccessToken(); - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'audit-auth-error'); - return; - } - const endpoint = galacticAdminMode ? `${API_BASE_URL}/api/galactic/audit/logs` : `${API_BASE_URL}/api/audit/logs`; - const response = await fetch(endpoint, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(endpoint, getAccessToken); if (!response.ok) { addNotification('error', 'Backend Error', `Failed to load audit logs: ${response.statusText}`, 'audit-fetch-error'); @@ -79,8 +70,12 @@ export default function AuditPage() { addNotification('error', 'Backend Error', data.message || 'Failed to load audit logs', 'audit-fetch-error'); } } catch (err) { - console.error('Error fetching audit logs:', err); - addNotification('error', 'Backend Not Reachable', 'Unable to load audit logs. Please check your connection.', 'audit-fetch-error'); + if (err instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', err.message, 'session-expired-error'); + } else { + console.error('Error fetching audit logs:', err); + addNotification('error', 'Backend Not Reachable', 'Unable to load audit logs. Please check your connection.', 'audit-fetch-error'); + } } finally { setLoading(false); setRefreshing(false); diff --git a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx index 7d6030d8..3240c5f1 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx +++ b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx @@ -9,6 +9,7 @@ import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; import { useNotifications } from "../../contexts/NotificationContext"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface Session { sessionId: string; @@ -87,19 +88,7 @@ export default function Home() { ? `${API_BASE_URL}/api/galactic/sessions` : `${API_BASE_URL}/api/sessions?tenantId=${tenantId}`; - // Get access token and include Authorization header - const token = await getAccessToken(); - - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'auth-error'); - return; - } - - const response = await fetch(endpoint, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { const data = await response.json(); @@ -110,8 +99,12 @@ export default function Home() { addNotification('error', 'Backend Error', `Failed to fetch sessions: ${response.statusText}`, 'backend-error'); } } catch (error) { - console.error("Failed to fetch sessions:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to connect to the backend API. Please ensure the backend server is running.', 'backend-unreachable'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch sessions:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to connect to the backend API. Please ensure the backend server is running.', 'backend-unreachable'); + } } finally { setLoading(false); } @@ -124,9 +117,6 @@ export default function Home() { } try { - const token = await getAccessToken(); - if (!token) return; - const tenantIds = galacticAdminMode ? [...new Set(currentSessions.map(s => s.tenantId))] : tenantId ? [tenantId] : []; @@ -138,9 +128,8 @@ export default function Home() { const results = await Promise.allSettled( tenantIds.map(tid => - fetch(`${API_BASE_URL}/api/devices/blocked?tenantId=${encodeURIComponent(tid)}`, { - headers: { Authorization: `Bearer ${token}` }, - }).then(res => res.ok ? res.json() : { blocked: [] }) + authenticatedFetch(`${API_BASE_URL}/api/devices/blocked?tenantId=${encodeURIComponent(tid)}`, getAccessToken) + .then(res => res.ok ? res.json() : { blocked: [] }) ) ); @@ -155,7 +144,11 @@ export default function Home() { setBlockedDevicesSet(newSet); } catch (error) { - console.error("Failed to fetch blocked devices:", error); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch blocked devices:", error); + } } }; @@ -172,14 +165,7 @@ export default function Home() { if (user && !user.isTenantAdmin && !user.isGalacticAdmin) return; try { - const token = await getAccessToken(); - if (!token) return; - - const response = await fetch(`${API_BASE_URL}/api/config/${tenantId}`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/config/${tenantId}`, getAccessToken); if (!response.ok) { setSerialValidationEnabled(null); @@ -188,7 +174,10 @@ export default function Home() { const data: TenantConfigurationSummary = await response.json(); setSerialValidationEnabled(!!data.validateAutopilotDevice); - } catch { + } catch (error) { + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } setSerialValidationEnabled(null); } }; @@ -413,19 +402,8 @@ export default function Home() { if (!sessionToDelete) return; try { - // Get access token and include Authorization header - const token = await getAccessToken(); - - if (!token) { - alert('Authentication error: Failed to get access token. Please try logging in again.'); - return; - } - - const response = await fetch(`${API_BASE_URL}/api/sessions/${sessionToDelete.sessionId}?tenantId=${sessionToDelete.tenantId}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/sessions/${sessionToDelete.sessionId}?tenantId=${sessionToDelete.tenantId}`, getAccessToken, { method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}` - } }); if (response.ok) { @@ -439,8 +417,12 @@ export default function Home() { alert(`Fehler beim Löschen: ${data.message || 'Unbekannter Fehler'}`); } } catch (error) { - console.error('Failed to delete session:', error); - alert('Fehler beim Löschen der Session'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error('Failed to delete session:', error); + alert('Fehler beim Löschen der Session'); + } } }; @@ -459,19 +441,10 @@ export default function Home() { try { setBlockingDevice(true); - const token = await getAccessToken(); - if (!token) { - alert('Authentication error: Failed to get access token. Please try logging in again.'); - return; - } - - const response = await fetch(`${API_BASE_URL}/api/devices/block`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/devices/block`, getAccessToken, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenantId: sessionToBlock.tenantId, serialNumber: sessionToBlock.serialNumber, @@ -495,8 +468,12 @@ export default function Home() { alert(`Fehler beim Blocken: ${data.message || 'Unbekannter Fehler'}`); } } catch (error) { - console.error('Failed to block device:', error); - alert('Fehler beim Blocken des Geräts'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error('Failed to block device:', error); + alert('Fehler beim Blocken des Geräts'); + } } finally { setBlockingDevice(false); } @@ -1314,26 +1291,18 @@ function SettingsMenu({ const fetchAuditLogs = async () => { setLoadingLogs(true); try { - // Get access token and include Authorization header - const token = await getAccessToken(); - - if (!token) { - console.error('Failed to get access token for audit logs'); - return; - } - - const response = await fetch(`${API_BASE_URL}/api/audit/logs?tenantId=${tenantId}`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/audit/logs?tenantId=${tenantId}`, getAccessToken); if (response.ok) { const data = await response.json(); setAuditLogs(data.logs || []); } } catch (error) { - console.error("Failed to fetch audit logs:", error); + if (error instanceof TokenExpiredError) { + console.error('Session expired:', error.message); + } else { + console.error("Failed to fetch audit logs:", error); + } } finally { setLoadingLogs(false); } diff --git a/src/Web/autopilot-monitor-web/app/diagnosis/[sessionId]/page.tsx b/src/Web/autopilot-monitor-web/app/diagnosis/[sessionId]/page.tsx index 37a43812..78fda486 100644 --- a/src/Web/autopilot-monitor-web/app/diagnosis/[sessionId]/page.tsx +++ b/src/Web/autopilot-monitor-web/app/diagnosis/[sessionId]/page.tsx @@ -8,6 +8,7 @@ import { useAuth } from "../../../contexts/AuthContext"; import { useNotifications } from "../../../contexts/NotificationContext"; import { ProtectedRoute } from "../../../components/ProtectedRoute"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface EnrollmentEvent { eventId: string; @@ -157,14 +158,7 @@ export default function DiagnosisPage() { const endpoint = galacticAdminMode ? `${API_BASE_URL}/api/galactic/sessions` : `${API_BASE_URL}/api/sessions?tenantId=${tenantId}`; - const token = await getAccessToken(); - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'diagnosis-auth-error'); - return; - } - const response = await fetch(endpoint, { - headers: { Authorization: `Bearer ${token}` }, - }); + const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { const data = await response.json(); const found = data.sessions?.find( @@ -178,19 +172,21 @@ export default function DiagnosisPage() { addNotification('error', 'Backend Error', `Failed to load session: ${response.statusText}`, 'diagnosis-fetch-error'); } } catch (error) { - console.error("Failed to fetch session details:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to load session details. Please check your connection.', 'diagnosis-fetch-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch session details:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to load session details. Please check your connection.', 'diagnosis-fetch-error'); + } } }; const fetchEvents = async () => { const effectiveTenantId = sessionTenantId || tenantId; try { - const token = await getAccessToken(); - if (!token) return; - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/sessions/${sessionId}/events?tenantId=${effectiveTenantId}`, - { headers: { Authorization: `Bearer ${token}` } } + getAccessToken ); if (response.ok) { const data = await response.json(); @@ -199,22 +195,21 @@ export default function DiagnosisPage() { addNotification('error', 'Backend Error', `Failed to load events: ${response.statusText}`, 'diagnosis-events-error'); } } catch (error) { - console.error("Failed to fetch events:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to load session events. Please check your connection.', 'diagnosis-events-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch events:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to load session events. Please check your connection.', 'diagnosis-events-error'); + } } }; const fetchAnalysisResults = async () => { const effectiveTenantId = sessionTenantId || tenantId; try { - const token = await getAccessToken(); - if (!token) { - setLoading(false); - return; - } - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/sessions/${sessionId}/analysis?tenantId=${effectiveTenantId}`, - { headers: { Authorization: `Bearer ${token}` } } + getAccessToken ); if (response.ok) { const data = await response.json(); @@ -228,7 +223,11 @@ export default function DiagnosisPage() { } } } catch (error) { - console.error("Failed to fetch analysis results:", error); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch analysis results:", error); + } } finally { setLoading(false); } diff --git a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx index 98b9541d..fa8648df 100644 --- a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx +++ b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx @@ -8,6 +8,7 @@ import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; import { useNotifications } from "../../contexts/NotificationContext"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface Session { sessionId: string; @@ -141,14 +142,7 @@ export default function FleetHealthPage() { const endpoint = galacticAdminMode ? `${API_BASE_URL}/api/galactic/sessions` : `${API_BASE_URL}/api/sessions?tenantId=${tenantId}`; - const token = await getAccessToken(); - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'fleet-health-auth-error'); - return; - } - const response = await fetch(endpoint, { - headers: { Authorization: `Bearer ${token}` }, - }); + const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { const data = await response.json(); setSessions(data.sessions || []); @@ -156,8 +150,12 @@ export default function FleetHealthPage() { addNotification('error', 'Backend Error', `Failed to load sessions: ${response.statusText}`, 'fleet-health-sessions-error'); } } catch (error) { - console.error("Failed to fetch sessions:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to load fleet health data. Please check your connection.', 'fleet-health-sessions-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch sessions:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to load fleet health data. Please check your connection.', 'fleet-health-sessions-error'); + } } finally { setLoading(false); } @@ -165,16 +163,11 @@ export default function FleetHealthPage() { const fetchAppMetrics = async (range: "7d" | "30d" | "90d" = timeRange) => { try { - const token = await getAccessToken(); - if (!token) return; const days = range === "7d" ? 7 : range === "30d" ? 30 : 90; const endpoint = galacticAdminMode ? `${API_BASE_URL}/api/galactic/metrics/app?days=${days}` : `${API_BASE_URL}/api/metrics/app?tenantId=${tenantId}&days=${days}`; - const response = await fetch( - endpoint, - { headers: { Authorization: `Bearer ${token}` } } - ); + const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { const data = await response.json(); setAppMetrics(data); @@ -182,8 +175,12 @@ export default function FleetHealthPage() { addNotification('error', 'Backend Error', `Failed to load app metrics: ${response.statusText}`, 'fleet-health-metrics-error'); } } catch (error) { - console.error("Failed to fetch app metrics:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to load app metrics. Please check your connection.', 'fleet-health-metrics-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch app metrics:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to load app metrics. Please check your connection.', 'fleet-health-metrics-error'); + } } }; diff --git a/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx b/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx index 9f5602f7..845fc18c 100644 --- a/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx +++ b/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx @@ -7,6 +7,7 @@ import { ProtectedRoute } from "../../components/ProtectedRoute"; import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { downloadAsJson, stripInternalFields, bumpVersion } from "@/lib/rulePageHelpers"; import { StatCard } from "@/components/rules/StatCard"; import { RuleFilterBar } from "@/components/rules/RuleFilterBar"; @@ -190,16 +191,7 @@ export default function GatherRulesPage() { setLoading(true); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/gather`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/gather`, getAccessToken); if (!response.ok) { throw new Error(`Failed to load gather rules: ${response.statusText}`); @@ -229,16 +221,10 @@ export default function GatherRulesPage() { setTogglingRule(rule.ruleId); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - - const response = await fetch(`${API_BASE_URL}/api/rules/gather/${encodeURIComponent(rule.ruleId)}`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/gather/${encodeURIComponent(rule.ruleId)}`, getAccessToken, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify({ ...rule, enabled: !rule.enabled }), }); @@ -270,17 +256,9 @@ export default function GatherRulesPage() { setDeletingRule(rule.ruleId); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - const url = `${API_BASE_URL}/api/rules/gather/${encodeURIComponent(rule.ruleId)}`; - const response = await fetch(url, { + const response = await authenticatedFetch(url, getAccessToken, { method: "DELETE", - headers: { - Authorization: `Bearer ${token}`, - }, }); if (!response.ok) { @@ -333,11 +311,6 @@ export default function GatherRulesPage() { setCreating(true); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - const payload: Record = { ruleId: formData.ruleId.trim(), title: formData.title.trim(), @@ -361,11 +334,10 @@ export default function GatherRulesPage() { payload.triggerEventType = formData.triggerEventType.trim(); } - const response = await fetch(`${API_BASE_URL}/api/rules/gather`, { + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/gather`, getAccessToken, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), }); @@ -429,11 +401,6 @@ export default function GatherRulesPage() { setSaving(true); setError(null); - const token = await getAccessToken(); - if (!token) { - throw new Error("Failed to get access token"); - } - const payload: Record = { ...rule, title: formData.title.trim(), @@ -461,11 +428,10 @@ export default function GatherRulesPage() { const url = `${API_BASE_URL}/api/rules/gather/${encodeURIComponent(rule.ruleId)}`; - const response = await fetch(url, { + const response = await authenticatedFetch(url, getAccessToken, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), }); diff --git a/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx b/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx index adaa3f7c..7741e40e 100644 --- a/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx +++ b/src/Web/autopilot-monitor-web/app/geographic-performance/page.tsx @@ -7,6 +7,7 @@ import { ProtectedRoute } from "../../components/ProtectedRoute"; import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; // Dynamically import the map component (Leaflet requires window/document) const GeoMap = dynamic(() => import("./GeoMap"), { ssr: false }); @@ -113,21 +114,21 @@ export default function GeographicPerformancePage() { const fetchGeoMetrics = useCallback(async (range: TimeRange = timeRange, group: GroupBy = groupBy) => { try { - const token = await getAccessToken(); - if (!token) return; const days = range === "7d" ? 7 : range === "30d" ? 30 : 90; const endpoint = galacticAdminMode ? `${API_BASE_URL}/api/galactic/metrics/geographic?days=${days}&groupBy=${group}` : `${API_BASE_URL}/api/metrics/geographic?tenantId=${tenantId}&days=${days}&groupBy=${group}`; - const response = await fetch(endpoint, { - headers: { Authorization: `Bearer ${token}` }, - }); + const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { const data = await response.json(); setGeoMetrics(data); } } catch (error) { - console.error("Failed to fetch geographic metrics:", error); + if (error instanceof TokenExpiredError) { + console.error("Session expired:", error.message); + } else { + console.error("Failed to fetch geographic metrics:", error); + } } finally { setLoading(false); } diff --git a/src/Web/autopilot-monitor-web/app/health-check/page.tsx b/src/Web/autopilot-monitor-web/app/health-check/page.tsx index 97e21385..a86cc5cd 100644 --- a/src/Web/autopilot-monitor-web/app/health-check/page.tsx +++ b/src/Web/autopilot-monitor-web/app/health-check/page.tsx @@ -4,6 +4,7 @@ import { useAuth } from '@/contexts/AuthContext'; import { useNotifications } from '@/contexts/NotificationContext'; import { useState, useEffect } from 'react'; import { API_BASE_URL } from '@/lib/config'; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface HealthCheck { name: string; @@ -30,17 +31,7 @@ export default function HealthCheckPage() { const performHealthCheck = async () => { setLoading(true); try { - const token = await getAccessToken(); - if (!token) { - addNotification('error', 'Authentication Error', 'No access token available', 'health-check-auth-error'); - return; - } - - const response = await fetch(`${API_BASE_URL}/api/health/detailed`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/health/detailed`, getAccessToken); if (!response.ok) { if (response.status === 403) { @@ -60,8 +51,12 @@ export default function HealthCheckPage() { addNotification('warning', 'Health Check Complete', 'Some systems are unhealthy'); } } catch (error) { - console.error('Health check error:', error); - addNotification('error', 'Health Check Error', error instanceof Error ? error.message : 'Unknown error', 'health-check-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error('Health check error:', error); + addNotification('error', 'Health Check Error', error instanceof Error ? error.message : 'Unknown error', 'health-check-error'); + } } finally { setLoading(false); } diff --git a/src/Web/autopilot-monitor-web/app/ime-log-patterns/page.tsx b/src/Web/autopilot-monitor-web/app/ime-log-patterns/page.tsx index fdffd4c7..b8f3e723 100644 --- a/src/Web/autopilot-monitor-web/app/ime-log-patterns/page.tsx +++ b/src/Web/autopilot-monitor-web/app/ime-log-patterns/page.tsx @@ -7,6 +7,7 @@ import { ProtectedRoute } from "../../components/ProtectedRoute"; import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { downloadAsJson, stripInternalFields } from "@/lib/rulePageHelpers"; import { StatCard } from "@/components/rules/StatCard"; import { RuleFilterBar } from "@/components/rules/RuleFilterBar"; @@ -148,12 +149,7 @@ export default function ImeLogPatternsPage() { if (!tenantId) return; try { setLoading(true); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - - const response = await fetch(`${API_BASE_URL}/api/rules/ime-log-patterns`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/rules/ime-log-patterns`, getAccessToken); if (!response.ok) { throw new Error(`Failed to fetch patterns: ${response.statusText}`); @@ -166,8 +162,12 @@ export default function ImeLogPatternsPage() { throw new Error(data.message || "Failed to fetch patterns"); } } catch (err) { - console.error("Error fetching patterns:", err); - showError(err instanceof Error ? err.message : "Failed to fetch patterns"); + if (err instanceof TokenExpiredError) { + showError(err.message); + } else { + console.error("Error fetching patterns:", err); + showError(err instanceof Error ? err.message : "Failed to fetch patterns"); + } } finally { setLoading(false); } @@ -185,16 +185,13 @@ export default function ImeLogPatternsPage() { setTogglingPattern(pattern.patternId); setError(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/rules/ime-log-patterns/${encodeURIComponent(pattern.patternId)}?global=true`, + getAccessToken, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify({ ...pattern, enabled: !pattern.enabled }), } @@ -245,9 +242,6 @@ export default function ImeLogPatternsPage() { setSaving(true); setError(null); - const token = await getAccessToken(); - if (!token) throw new Error("Failed to get access token"); - const payload: Partial = { patternId, category: editForm.category, @@ -258,13 +252,13 @@ export default function ImeLogPatternsPage() { enabled: editForm.enabled, }; - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/rules/ime-log-patterns/${encodeURIComponent(patternId)}?global=true`, + getAccessToken, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), } diff --git a/src/Web/autopilot-monitor-web/app/platform-metrics/page.tsx b/src/Web/autopilot-monitor-web/app/platform-metrics/page.tsx index 06aa5b4d..413ed9c9 100644 --- a/src/Web/autopilot-monitor-web/app/platform-metrics/page.tsx +++ b/src/Web/autopilot-monitor-web/app/platform-metrics/page.tsx @@ -6,6 +6,7 @@ import { API_BASE_URL } from '@/lib/config'; import { ProtectedRoute } from '../../components/ProtectedRoute'; import { useAuth } from '../../contexts/AuthContext'; import { useNotifications } from '../../contexts/NotificationContext'; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; // ── Types ────────────────────────────────────────────────────────────────────── @@ -110,15 +111,7 @@ export default function PlatformMetricsPage() { setError(null); try { - const token = await getAccessToken(); - if (!token) { - addNotification('error', 'Auth Error', 'No access token', 'pm-auth'); - return; - } - - const res = await fetch(`${API_BASE_URL}/api/galactic/metrics/platform`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await authenticatedFetch(`${API_BASE_URL}/api/galactic/metrics/platform`, getAccessToken); if (!res.ok) { if (res.status === 403) { @@ -157,8 +150,12 @@ export default function PlatformMetricsPage() { setSessionMetrics(mapped); } catch (err) { - console.error('Platform metrics fetch error:', err); - setError(err instanceof Error ? err.message : 'Unknown error'); + if (err instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', err.message, 'session-expired-error'); + } else { + console.error('Platform metrics fetch error:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + } } finally { setLoading(false); } diff --git a/src/Web/autopilot-monitor-web/app/platform-usage-metrics/page.tsx b/src/Web/autopilot-monitor-web/app/platform-usage-metrics/page.tsx index 7ee89964..4d5c982a 100644 --- a/src/Web/autopilot-monitor-web/app/platform-usage-metrics/page.tsx +++ b/src/Web/autopilot-monitor-web/app/platform-usage-metrics/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'; import { API_BASE_URL } from '@/lib/config'; import { ProtectedRoute } from '../../components/ProtectedRoute'; import { useAuth } from '../../contexts/AuthContext'; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; interface SessionMetrics { total: number; @@ -98,17 +99,7 @@ export default function PlatformUsageMetricsPage() { setError(null); // Platform-wide metrics - cross-tenant (Galactic Admin only) - - const token = await getAccessToken(); - if (!token) { - throw new Error('Failed to get access token'); - } - - const response = await fetch(`${API_BASE_URL}/api/galactic/metrics/usage`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(`${API_BASE_URL}/api/galactic/metrics/usage`, getAccessToken); if (!response.ok) { throw new Error(`Failed to fetch platform usage metrics: ${response.statusText}`); @@ -117,7 +108,11 @@ export default function PlatformUsageMetricsPage() { const data = await response.json(); setMetrics(data); } catch (err) { - console.error('Error fetching platform usage metrics:', err); + if (err instanceof TokenExpiredError) { + console.error('Session expired:', err.message); + } else { + console.error('Error fetching platform usage metrics:', err); + } setError(err instanceof Error ? err.message : 'Failed to fetch platform usage metrics'); } finally { setLoading(false); diff --git a/src/Web/autopilot-monitor-web/app/progress/page.tsx b/src/Web/autopilot-monitor-web/app/progress/page.tsx index d2f10953..44a933a4 100644 --- a/src/Web/autopilot-monitor-web/app/progress/page.tsx +++ b/src/Web/autopilot-monitor-web/app/progress/page.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useMemo } from "react"; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; import { useNotifications } from "../../contexts/NotificationContext"; @@ -173,11 +174,9 @@ export default function ProgressPortalPage() { lastFetchedSessionId.current = session.sessionId; const fetchEvents = async () => { try { - const token = await getAccessToken(); - if (!token) return; - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/sessions/${session.sessionId}/events?tenantId=${tenantId}`, - { headers: { Authorization: `Bearer ${token}` } } + getAccessToken ); if (response.ok) { const data = await response.json(); @@ -195,8 +194,12 @@ export default function ProgressPortalPage() { addNotification('error', 'Backend Error', `Failed to load enrollment events: ${response.statusText}`, 'progress-events-error'); } } catch (error) { - console.error("Failed to fetch events:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to load enrollment events. Please check your connection.', 'progress-events-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch events:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to load enrollment events. Please check your connection.', 'progress-events-error'); + } } }; fetchEvents(); @@ -213,16 +216,9 @@ export default function ProgressPortalPage() { lastFetchedSessionId.current = null; try { - const token = await getAccessToken(); - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'progress-auth-error'); - setSearching(false); - return; - } - - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/sessions?tenantId=${tenantId}`, - { headers: { Authorization: `Bearer ${token}` } } + getAccessToken ); if (response.ok) { @@ -256,8 +252,12 @@ export default function ProgressPortalPage() { setNotFound(true); } } catch (error) { - console.error("Search failed:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to search for device. Please check your connection.', 'progress-search-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Search failed:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to search for device. Please check your connection.', 'progress-search-error'); + } setNotFound(true); } finally { setSearching(false); diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx index b25bbc9f..b5ff0891 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx @@ -12,6 +12,7 @@ import DownloadProgress from '../../../components/DownloadProgress'; import InstallProgress from '../../../components/InstallProgress'; import ScriptExecutions from '../../../components/ScriptExecutions'; import { API_BASE_URL } from "@/lib/config"; +import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { V1_PHASE_NAMES, V2_PHASE_NAMES, V1_PHASE_ORDER, V2_PHASE_ORDER } from "./utils/phaseConstants"; import { groupEventsByPhase } from "./utils/eventHelpers"; @@ -200,11 +201,10 @@ export default function SessionDetailPage() { // Fetch showScriptOutput setting (best-effort) (async () => { try { - const token = await getAccessToken(); - if (!token) return; - const res = await fetch(`${API_BASE_URL}/api/config/${sessionTenantId}`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await authenticatedFetch( + `${API_BASE_URL}/api/config/${sessionTenantId}`, + getAccessToken + ); if (res.ok) { const cfg = await res.json(); setShowScriptOutput(cfg.showScriptOutput ?? true); @@ -314,16 +314,6 @@ export default function SessionDetailPage() { const fetchSessionDetails = async () => { try { - // Get access token and include Authorization header - const token = await getAccessToken(); - - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'session-detail-auth-error'); - // Allow retry — token may not be ready yet (MSAL still initializing) - hasInitialFetch.current = false; - return; - } - const knownTenantId = resolveEffectiveTenantId(); const endpoint = knownTenantId ? `${API_BASE_URL}/api/sessions/${sessionId}?tenantId=${knownTenantId}` @@ -331,11 +321,7 @@ export default function SessionDetailPage() { ? `${API_BASE_URL}/api/galactic/sessions` : `${API_BASE_URL}/api/sessions/${sessionId}`; - const response = await fetch(endpoint, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { const data = await response.json(); const foundSession = data.session ?? data.sessions?.find((s: Session) => s.sessionId === sessionId); @@ -348,6 +334,10 @@ export default function SessionDetailPage() { addNotification('error', 'Backend Error', `Failed to load session details: ${response.statusText}`, 'session-detail-fetch-error'); } } catch (error) { + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + return; + } console.error("Failed to fetch session details:", error); addNotification('error', 'Backend Not Reachable', 'Unable to load session details. Please check your connection.', 'session-detail-fetch-error'); // Allow retry on network errors @@ -370,19 +360,10 @@ export default function SessionDetailPage() { fetchEventsQueued.current = false; try { - // Get access token and include Authorization header - const token = await getAccessToken(); - - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'session-events-auth-error'); - setLoading(false); - return; - } - const response = await fetch(`${API_BASE_URL}/api/sessions/${sessionId}/events?tenantId=${effectiveTenantId}`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch( + `${API_BASE_URL}/api/sessions/${sessionId}/events?tenantId=${effectiveTenantId}`, + getAccessToken + ); if (response.ok) { const data = await response.json(); // Replace entire event list with fresh data from Table Storage (canonical truth). @@ -403,8 +384,12 @@ export default function SessionDetailPage() { addNotification('error', 'Backend Error', `Failed to load session events: ${response.statusText}`, 'session-events-fetch-error'); } } catch (error) { - console.error("Failed to fetch events:", error); - addNotification('error', 'Backend Not Reachable', 'Unable to load session events. Please check your connection.', 'session-events-fetch-error'); + if (error instanceof TokenExpiredError) { + addNotification('error', 'Session Expired', error.message, 'session-expired-error'); + } else { + console.error("Failed to fetch events:", error); + addNotification('error', 'Backend Not Reachable', 'Unable to load session events. Please check your connection.', 'session-events-fetch-error'); + } } finally { setLoading(false); fetchEventsInFlight.current = false; @@ -422,13 +407,11 @@ export default function SessionDetailPage() { if (!effectiveTenantId || !GUID_REGEX.test(effectiveTenantId)) return; try { setLoadingAnalysis(true); - const token = await getAccessToken(); - if (!token) return; const reanalyzeParam = reanalyze ? '&reanalyze=true' : ''; - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/sessions/${sessionId}/analysis?tenantId=${effectiveTenantId}${reanalyzeParam}`, - { headers: { 'Authorization': `Bearer ${token}` } } + getAccessToken ); if (response.ok) { const data = await response.json(); @@ -452,19 +435,11 @@ export default function SessionDetailPage() { const effectiveTenantId = sessionTenantId || tenantId; try { - // Get access token and include Authorization header - const token = await getAccessToken(); - - if (!token) { - console.error('Failed to get access token for mark failed'); - return; - } - const response = await fetch(`${API_BASE_URL}/api/sessions/${sessionId}/mark-failed?tenantId=${effectiveTenantId}`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - } - }); + const response = await authenticatedFetch( + `${API_BASE_URL}/api/sessions/${sessionId}/mark-failed?tenantId=${effectiveTenantId}`, + getAccessToken, + { method: 'POST' } + ); if (response.ok) { setShowMarkFailedConfirm(false); @@ -495,11 +470,6 @@ export default function SessionDetailPage() { const effectiveTenantId = sessionTenantId || tenantId; try { setReportSubmitting(true); - const token = await getAccessToken(); - if (!token) { - addNotification('error', 'Authentication Error', 'Failed to get access token.', 'report-auth-error'); - throw new Error('Failed to get access token.'); - } // Generate TXT and CSV exports from the events currently loaded const exportEvents: SessionExportEvent[] = events.map(e => ({ @@ -511,14 +481,12 @@ export default function SessionDetailPage() { const sessionCsv = session ? generateSessionCsvExport(session) : ''; const ruleResultsCsv = generateRuleResultsCsvExport(analysisResults); - const response = await fetch( + const response = await authenticatedFetch( `${API_BASE_URL}/api/sessions/${sessionId}/report?tenantId=${effectiveTenantId}`, + getAccessToken, { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenantId: effectiveTenantId, sessionId, @@ -812,17 +780,19 @@ export default function SessionDetailPage() {