mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
better reconnect logic and added bing verification
This commit is contained in:
@@ -1 +1 @@
|
||||
242
|
||||
243
|
||||
|
||||
+25
-22
@@ -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);
|
||||
|
||||
+31
-51
@@ -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);
|
||||
|
||||
+6
-4
@@ -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);
|
||||
|
||||
+16
-16
@@ -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);
|
||||
|
||||
+26
-42
@@ -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);
|
||||
|
||||
+17
-28
@@ -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
Reference in New Issue
Block a user