better reconnect logic and added bing verification

This commit is contained in:
Oliver Kieselbach
2026-03-06 01:00:09 +01:00
parent 65245b6d62
commit 61c5937a94
28 changed files with 521 additions and 710 deletions
@@ -1 +1 @@
242
243
@@ -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);
@@ -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<string | null>;
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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),
});
@@ -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);
@@ -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);
}
@@ -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);
}
@@ -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');
}
}
};
@@ -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<string, unknown> = {
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<string, unknown> = {
...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),
});
@@ -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);
}
@@ -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);
}
@@ -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<ImeLogPattern> = {
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),
}
@@ -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);
}
@@ -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);
@@ -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);
@@ -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() {
<button
onClick={async () => {
try {
const token = await getAccessToken();
if (!token) return;
const res = await fetch(
const res = await authenticatedFetch(
`${API_BASE_URL}/api/diagnostics/download-url?tenantId=${session.tenantId}&blobName=${encodeURIComponent(session.diagnosticsBlobName!)}`,
{ headers: { Authorization: `Bearer ${token}` } }
getAccessToken
);
if (!res.ok) throw new Error('Failed to get download URL');
const data = await res.json();
window.open(data.downloadUrl, '_blank');
} catch (err) {
console.error('Diagnostics download failed:', err);
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error('Diagnostics download failed:', err);
}
}
}}
className="px-4 py-2 bg-white border border-gray-200 text-gray-700 rounded-md hover:bg-gray-50 transition-colors flex items-center gap-2 text-sm"
@@ -7,6 +7,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";
import UnsavedChangesModal from "../../components/UnsavedChangesModal";
import AutopilotValidationSection from "./components/AutopilotValidationSection";
import AdminManagementSection from "./components/AdminManagementSection";
@@ -205,16 +206,7 @@ export default function SettingsPage() {
setError(null);
const token = await getAccessToken();
if (!token) {
throw new Error('Failed to get access token');
}
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) {
throw new Error(`Failed to load configuration: ${response.statusText}`);
@@ -285,8 +277,12 @@ export default function SettingsPage() {
}
}
} catch (err) {
console.error("Error fetching configuration:", err);
setError(err instanceof Error ? err.message : "Failed to load configuration");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error("Error fetching configuration:", err);
setError(err instanceof Error ? err.message : "Failed to load configuration");
}
} finally {
setLoading(false);
}
@@ -302,16 +298,7 @@ export default function SettingsPage() {
try {
setLoadingAdmins(true);
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`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const response = await authenticatedFetch(`${API_BASE_URL}/api/tenants/${tenantId}/admins`, getAccessToken);
if (!response.ok) {
throw new Error(`Failed to load admins: ${response.statusText}`);
@@ -320,8 +307,12 @@ export default function SettingsPage() {
const data: TenantAdmin[] = await response.json();
setAdmins(data);
} catch (err) {
console.error("Error fetching admins:", err);
setError(err instanceof Error ? err.message : "Failed to load admins");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error("Error fetching admins:", err);
setError(err instanceof Error ? err.message : "Failed to load admins");
}
} finally {
setLoadingAdmins(false);
}
@@ -342,11 +333,7 @@ export default function SettingsPage() {
useEffect(() => {
const fetchGlobalDiagPaths = async () => {
try {
const token = await getAccessToken();
if (!token) return;
const res = await fetch(`${API_BASE_URL}/api/global/config`, {
headers: { Authorization: `Bearer ${token}` },
});
const res = await authenticatedFetch(`${API_BASE_URL}/api/global/config`, getAccessToken);
if (!res.ok) return;
const data = await res.json();
if (data.diagnosticsGlobalLogPathsJson) {
@@ -477,16 +464,10 @@ export default function SettingsPage() {
localAdminAllowedAccountsJson: localAdminAllowedAccounts.length > 0 ? JSON.stringify(localAdminAllowedAccounts) : undefined,
};
const token = await getAccessToken();
if (!token) {
throw new Error('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}/api/config/${tenantId}`, {
const response = await authenticatedFetch(`${API_BASE_URL}/api/config/${tenantId}`, getAccessToken, {
method: "PUT",
headers: {
"Content-Type": "application/json",
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(updatedConfig),
});
@@ -515,19 +496,10 @@ export default function SettingsPage() {
setError(null);
setSuccessMessage(null);
const token = await getAccessToken();
if (!token) {
throw new Error("Failed to get access token");
}
const redirectUri = `${window.location.origin}/settings`;
const response = await fetch(
const response = await authenticatedFetch(
`${API_BASE_URL}/api/config/${tenantId}/autopilot-device-validation/consent-url?redirectUri=${encodeURIComponent(redirectUri)}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
getAccessToken,
);
if (!response.ok) {
@@ -544,7 +516,11 @@ export default function SettingsPage() {
sessionStorage.setItem("consentTrigger", trigger);
window.location.href = data.consentUrl;
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to start admin consent flow");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
setError(err instanceof Error ? err.message : "Failed to start admin consent flow");
}
setAutopilotConsentInProgress(false);
}
};
@@ -584,18 +560,10 @@ export default function SettingsPage() {
try {
setAutopilotConsentInProgress(true);
const token = await getAccessToken();
if (!token) {
throw new Error("Failed to get access token");
}
const statusResponse = await fetch(
const statusResponse = await authenticatedFetch(
`${API_BASE_URL}/api/config/${tenantId}/autopilot-device-validation/consent-status`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
getAccessToken,
);
if (!statusResponse.ok) {
@@ -617,7 +585,11 @@ export default function SettingsPage() {
}
router.replace("/settings");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to verify consent");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
setError(err instanceof Error ? err.message : "Failed to verify consent");
}
} finally {
setAutopilotConsentInProgress(false);
}
@@ -632,8 +604,12 @@ export default function SettingsPage() {
try {
await saveConfiguration();
} catch (err) {
console.error("Error saving configuration:", err);
setError(err instanceof Error ? err.message : "Failed to save configuration");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error("Error saving configuration:", err);
setError(err instanceof Error ? err.message : "Failed to save configuration");
}
}
};
@@ -646,16 +622,10 @@ export default function SettingsPage() {
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/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}`
},
body: JSON.stringify({ upn: newAdminEmail.trim() }),
});
@@ -674,8 +644,12 @@ export default function SettingsPage() {
// Auto-hide success message after 3 seconds
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err) {
console.error("Error adding admin:", err);
setError(err instanceof Error ? err.message : "Failed to add admin");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error("Error adding admin:", err);
setError(err instanceof Error ? err.message : "Failed to add admin");
}
} finally {
setAddingAdmin(false);
}
@@ -694,16 +668,8 @@ export default function SettingsPage() {
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/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) {
@@ -719,8 +685,12 @@ export default function SettingsPage() {
// Auto-hide success message after 3 seconds
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err) {
console.error("Error removing admin:", err);
setError(err instanceof Error ? err.message : "Failed to remove admin");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error("Error removing admin:", err);
setError(err instanceof Error ? err.message : "Failed to remove admin");
}
} finally {
setRemovingAdmin(null);
}
@@ -736,19 +706,12 @@ export default function SettingsPage() {
setError(null);
setSuccessMessage(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/tenants/${tenantId}/admins/${encodeURIComponent(adminUpn)}/${action}`,
getAccessToken,
{
method: "PATCH",
headers: {
'Authorization': `Bearer ${token}`
},
}
},
);
if (!response.ok) {
@@ -769,8 +732,12 @@ export default function SettingsPage() {
// Auto-hide success message after 3 seconds
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err) {
console.error(`Error ${action}ing admin:`, err);
setError(err instanceof Error ? err.message : `Failed to ${action} admin`);
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error(`Error ${action}ing admin:`, err);
setError(err instanceof Error ? err.message : `Failed to ${action} admin`);
}
} finally {
setTogglingAdmin(null);
}
@@ -782,11 +749,9 @@ export default function SettingsPage() {
if (!tenantId) return;
try {
setBootstrapLoading(true);
const token = await getAccessToken();
if (!token) return;
const response = await fetch(
const response = await authenticatedFetch(
`${API_BASE_URL}/api/bootstrap/sessions?tenantId=${tenantId}`,
{ headers: { Authorization: `Bearer ${token}` } }
getAccessToken,
);
if (response.ok) {
const data = await response.json();
@@ -802,13 +767,10 @@ export default function SettingsPage() {
const createBootstrapSession = async (validityHours: number, label: string): Promise<string | null> => {
if (!tenantId) return null;
try {
const token = await getAccessToken();
if (!token) throw new Error("Failed to get access token");
const response = await fetch(`${API_BASE_URL}/api/bootstrap/sessions`, {
const response = await authenticatedFetch(`${API_BASE_URL}/api/bootstrap/sessions`, getAccessToken, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ tenantId, validityHours, label }),
});
@@ -820,7 +782,11 @@ export default function SettingsPage() {
await fetchBootstrapSessions();
return data.bootstrapUrl || null;
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create bootstrap session");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
setError(err instanceof Error ? err.message : "Failed to create bootstrap session");
}
return null;
}
};
@@ -828,11 +794,10 @@ export default function SettingsPage() {
const revokeBootstrapSession = async (code: string) => {
if (!tenantId) return;
try {
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/bootstrap/sessions/${code}?tenantId=${tenantId}`,
{ method: "DELETE", headers: { Authorization: `Bearer ${token}` } }
getAccessToken,
{ method: "DELETE" },
);
if (!response.ok) {
const data = await response.json().catch(() => ({}));
@@ -840,7 +805,11 @@ export default function SettingsPage() {
}
await fetchBootstrapSessions();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to revoke bootstrap session");
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
setError(err instanceof Error ? err.message : "Failed to revoke bootstrap session");
}
}
};
@@ -851,12 +820,8 @@ export default function SettingsPage() {
setOffboarding(true);
setOffboardError(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}/offboard`, {
const response = await authenticatedFetch(`${API_BASE_URL}/api/tenants/${tenantId}/offboard`, getAccessToken, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` },
});
if (!response.ok) {
@@ -867,7 +832,11 @@ export default function SettingsPage() {
// Offboard successful – sign out the user as their admin access is gone
logout();
} catch (err) {
setOffboardError(err instanceof Error ? err.message : 'Offboard failed');
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
setOffboardError(err instanceof Error ? err.message : 'Offboard failed');
}
setOffboarding(false);
}
};
@@ -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 SessionMetrics {
total: number;
@@ -87,17 +88,7 @@ export default function UsageMetricsPage() {
setLoading(true);
}
const token = await getAccessToken();
if (!token) {
addNotification('error', 'Authentication Error', 'Failed to get access token. Please try logging in again.', 'usage-metrics-auth-error');
return;
}
const response = await fetch(`${API_BASE_URL}/api/metrics/usage?tenantId=${tenantId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const response = await authenticatedFetch(`${API_BASE_URL}/api/metrics/usage?tenantId=${tenantId}`, getAccessToken);
if (!response.ok) {
addNotification('error', 'Backend Error', `Failed to load usage metrics: ${response.statusText}`, 'usage-metrics-fetch-error');
@@ -107,8 +98,12 @@ export default function UsageMetricsPage() {
const data = await response.json();
setMetrics(data);
} catch (err) {
console.error('Error fetching usage metrics:', err);
addNotification('error', 'Backend Not Reachable', 'Unable to load usage metrics. Please check your connection.', 'usage-metrics-fetch-error');
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
} else {
console.error('Error fetching usage metrics:', err);
addNotification('error', 'Backend Not Reachable', 'Unable to load usage metrics. Please check your connection.', 'usage-metrics-fetch-error');
}
} finally {
setLoading(false);
setRefreshing(false);
@@ -34,7 +34,7 @@ interface AuthContextType {
previewMessage: string;
login: () => Promise<void>;
logout: () => Promise<void>;
getAccessToken: () => Promise<string | null>;
getAccessToken: (forceRefresh?: boolean) => Promise<string | null>;
refreshUserInfo: () => Promise<void>;
}
@@ -224,7 +224,7 @@ function AuthProviderInternal({ children }: { children: React.ReactNode }) {
* Gets access token for API calls
* Automatically handles token refresh
*/
const getAccessToken = useCallback(async (): Promise<string | null> => {
const getAccessToken = useCallback(async (forceRefresh?: boolean): Promise<string | null> => {
if (accounts.length === 0) {
return null;
}
@@ -233,6 +233,7 @@ function AuthProviderInternal({ children }: { children: React.ReactNode }) {
const response = await instance.acquireTokenSilent({
scopes: apiRequest.scopes,
account: accounts[0],
forceRefresh: forceRefresh ?? false,
});
return response.accessToken;
@@ -3,6 +3,7 @@
import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react';
import * as signalR from '@microsoft/signalr';
import { API_BASE_URL } from '@/lib/config';
import { authenticatedFetch } from '@/lib/authenticatedFetch';
import { useAuth } from './AuthContext';
interface SignalRContextType {
@@ -81,15 +82,15 @@ export function SignalRProvider({ children }: { children: React.ReactNode }) {
for (const groupName of previousGroups) {
try {
const token = await getAccessToken();
const response = await fetch(`${API_BASE_URL}/api/realtime/groups/join`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ connectionId, groupName })
});
const response = await authenticatedFetch(
`${API_BASE_URL}/api/realtime/groups/join`,
getAccessToken,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connectionId, groupName })
}
);
if (response.ok) {
joinedGroupsRef.current.add(groupName);
@@ -178,15 +179,15 @@ export function SignalRProvider({ children }: { children: React.ReactNode }) {
return;
}
const token = await getAccessToken();
const response = await fetch(`${API_BASE_URL}/api/realtime/groups/join`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ connectionId, groupName })
});
const response = await authenticatedFetch(
`${API_BASE_URL}/api/realtime/groups/join`,
getAccessToken,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connectionId, groupName })
}
);
if (!response.ok) {
// Remove from Set if API call failed (so we can retry)
@@ -218,15 +219,15 @@ export function SignalRProvider({ children }: { children: React.ReactNode }) {
return;
}
const token = await getAccessToken();
const response = await fetch(`${API_BASE_URL}/api/realtime/groups/leave`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ connectionId, groupName })
});
const response = await authenticatedFetch(
`${API_BASE_URL}/api/realtime/groups/leave`,
getAccessToken,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connectionId, groupName })
}
);
if (!response.ok) {
// Re-add to Set if API call failed (so we can retry)
@@ -0,0 +1,52 @@
/**
* Authenticated fetch wrapper with automatic 401 retry.
*
* On a 401 response the wrapper forces an MSAL token refresh and retries the
* request once. If the retry also fails with 401, a `TokenExpiredError` is
* thrown so callers can show a clear "session expired" message instead of a
* generic backend error.
*/
export class TokenExpiredError extends Error {
constructor() {
super('Your session has expired. Please reload the page to sign in again.');
this.name = 'TokenExpiredError';
}
}
type GetAccessToken = (forceRefresh?: boolean) => Promise<string | null>;
export async function authenticatedFetch(
url: string,
getAccessToken: GetAccessToken,
init?: RequestInit,
): Promise<Response> {
const token = await getAccessToken();
if (!token) {
throw new TokenExpiredError();
}
const headers = new Headers(init?.headers);
headers.set('Authorization', `Bearer ${token}`);
const response = await fetch(url, { ...init, headers });
if (response.status === 401) {
// Force MSAL to bypass its cache and obtain a fresh token.
const freshToken = await getAccessToken(true);
if (!freshToken) {
throw new TokenExpiredError();
}
const retryHeaders = new Headers(init?.headers);
retryHeaders.set('Authorization', `Bearer ${freshToken}`);
const retryResponse = await fetch(url, { ...init, headers: retryHeaders });
if (retryResponse.status === 401) {
throw new TokenExpiredError();
}
return retryResponse;
}
return response;
}
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<users>
<user>D4BE6F80B00107FCF85444F56D520D3F</user>
</users>
File diff suppressed because one or more lines are too long