Fix edge gateway cache authorization fallback
This commit is contained in:
@@ -59,6 +59,7 @@ import {
|
||||
setReleaseChannelSwitchNoticePrincipal,
|
||||
} from "@/services/releaseChannelAvailability.js";
|
||||
import { clearCachedXlvaskUsageAmount } from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
import {
|
||||
clearPeriodCache,
|
||||
clearSelfWashCountsCache,
|
||||
@@ -107,6 +108,7 @@ const clearPrivilegedSessionCaches = () => {
|
||||
clearPeriodCache();
|
||||
clearSelfWashCountsCache();
|
||||
clearCachedXlvaskUsageAmount();
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
};
|
||||
|
||||
const clearStoredSession = () => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import {
|
||||
cancelEdgeGatewayOperation,
|
||||
clearEdgeGatewayWorkspaceCache,
|
||||
createEdgeGatewayInstallToken,
|
||||
createEdgeGatewayOperation,
|
||||
deleteEdgeGateway,
|
||||
@@ -12,10 +13,12 @@ import {
|
||||
getEdgeGatewayStatistics,
|
||||
getEdgeGatewayTasks,
|
||||
listEdgeGatewayDepartments,
|
||||
isEdgeGatewayAuthorizationError,
|
||||
listEdgeGateways,
|
||||
peekCachedEdgeGateway,
|
||||
peekCachedEdgeGatewayDepartments,
|
||||
peekCachedEdgeGatewayList,
|
||||
removeEdgeGatewayCache,
|
||||
rotateEdgeGatewayCredentials,
|
||||
saveEdgeGatewayBindings,
|
||||
setDepartmentGatewayCutover,
|
||||
@@ -1215,6 +1218,15 @@ const refreshSelected = async (gatewayId = activeGatewayId.value, { forceRefresh
|
||||
}
|
||||
return gateway;
|
||||
} catch (error) {
|
||||
if (isEdgeGatewayAuthorizationError(error)) {
|
||||
removeEdgeGatewayCache(gatewayId);
|
||||
unavailableGatewayId.value = String(gatewayId);
|
||||
selectedGateway.value = null;
|
||||
resetGatewayViewSnapshots();
|
||||
fail(error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackGateway = findLocalGatewaySnapshot(gatewayId);
|
||||
if (fallbackGateway) {
|
||||
setSelectedGatewaySnapshot(fallbackGateway);
|
||||
@@ -1561,6 +1573,16 @@ const load = async () => {
|
||||
departments.value = unwrap(departmentsResponse, []);
|
||||
await syncSelection();
|
||||
} catch (error) {
|
||||
if (isEdgeGatewayAuthorizationError(error)) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
gateways.value = [];
|
||||
fleetUsage.value = buildFleetUsageFromRows([]);
|
||||
if (activeGatewayId.value) {
|
||||
unavailableGatewayId.value = String(activeGatewayId.value);
|
||||
}
|
||||
selectedGateway.value = null;
|
||||
resetGatewayViewSnapshots();
|
||||
}
|
||||
fail(error);
|
||||
} finally {
|
||||
loading.value.init = false;
|
||||
|
||||
@@ -9,6 +9,7 @@ const listRequestsInFlight = new Map();
|
||||
const detailRequestsInFlight = new Map();
|
||||
let departmentsRequestInFlight = null;
|
||||
let edgeGatewayWorkspaceCache = null;
|
||||
let edgeGatewayWorkspaceCacheStorageKey = null;
|
||||
|
||||
const cloneJson = (value) => (value === null || value === undefined ? value : JSON.parse(JSON.stringify(value)));
|
||||
const normalizeDepartmentId = (departmentId) =>
|
||||
@@ -105,27 +106,55 @@ const storage = () => {
|
||||
return window.localStorage;
|
||||
};
|
||||
|
||||
const scopedWorkspaceCacheKey = () => {
|
||||
const localStorageHandle = storage();
|
||||
if (!localStorageHandle) {
|
||||
return EDGE_GATEWAY_WORKSPACE_CACHE_KEY;
|
||||
}
|
||||
|
||||
const sessionParts = [
|
||||
localStorageHandle.getItem("token") || "anonymous",
|
||||
localStorageHandle.getItem("is_subuser") === "true" ? "subuser" : "primary",
|
||||
localStorageHandle.getItem("selected_customer_number") || "none",
|
||||
];
|
||||
const principal = sessionParts.join("|");
|
||||
let hash = 0;
|
||||
for (let index = 0; index < principal.length; index += 1) {
|
||||
hash = (hash * 31 + principal.charCodeAt(index)) >>> 0;
|
||||
}
|
||||
|
||||
return `${EDGE_GATEWAY_WORKSPACE_CACHE_KEY}.${hash.toString(36)}`;
|
||||
};
|
||||
|
||||
const resetWorkspaceCacheMemory = (storageKey = scopedWorkspaceCacheKey()) => {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
|
||||
edgeGatewayWorkspaceCacheStorageKey = storageKey;
|
||||
return edgeGatewayWorkspaceCache;
|
||||
};
|
||||
|
||||
const loadWorkspaceCache = () => {
|
||||
if (edgeGatewayWorkspaceCache !== null) {
|
||||
const storageKey = scopedWorkspaceCacheKey();
|
||||
if (edgeGatewayWorkspaceCache !== null && edgeGatewayWorkspaceCacheStorageKey === storageKey) {
|
||||
return edgeGatewayWorkspaceCache;
|
||||
}
|
||||
|
||||
const localStorageHandle = storage();
|
||||
if (!localStorageHandle) {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
|
||||
return edgeGatewayWorkspaceCache;
|
||||
return resetWorkspaceCacheMemory(storageKey);
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = JSON.parse(localStorageHandle.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY) || "null");
|
||||
localStorageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
const decoded = JSON.parse(localStorageHandle.getItem(storageKey) || "null");
|
||||
edgeGatewayWorkspaceCache = {
|
||||
...createEmptyWorkspaceCache(),
|
||||
...(decoded && typeof decoded === "object" ? decoded : {}),
|
||||
lists: decoded?.lists && typeof decoded.lists === "object" ? decoded.lists : {},
|
||||
details: decoded?.details && typeof decoded.details === "object" ? decoded.details : {},
|
||||
};
|
||||
edgeGatewayWorkspaceCacheStorageKey = storageKey;
|
||||
} catch (_error) {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
|
||||
resetWorkspaceCacheMemory(storageKey);
|
||||
}
|
||||
|
||||
return edgeGatewayWorkspaceCache;
|
||||
@@ -137,7 +166,9 @@ const persistWorkspaceCache = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorageHandle.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(edgeGatewayWorkspaceCache));
|
||||
const storageKey = edgeGatewayWorkspaceCacheStorageKey || scopedWorkspaceCacheKey();
|
||||
localStorageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
localStorageHandle.setItem(storageKey, JSON.stringify(edgeGatewayWorkspaceCache));
|
||||
};
|
||||
|
||||
const mergeGatewaySnapshots = (currentGateway, nextGateway) => {
|
||||
@@ -295,6 +326,27 @@ const buildOperationPayload = (typeOrPayload, request = {}) => {
|
||||
export const unwrapEdgeGatewayResponse = (response, fallback = null) => response?.data?.data ?? fallback;
|
||||
export const unwrapEdgeGatewayMeta = (response) => response?.data?.meta ?? {};
|
||||
|
||||
export const isEdgeGatewayAuthorizationError = (error) => {
|
||||
const status = Number(error?.response?.status || error?.status || error?.response?.data?.status || 0);
|
||||
return status === 401 || status === 403;
|
||||
};
|
||||
|
||||
export const clearEdgeGatewayWorkspaceCache = () => {
|
||||
const localStorageHandle = storage();
|
||||
const storageKey = scopedWorkspaceCacheKey();
|
||||
resetWorkspaceCacheMemory(storageKey);
|
||||
if (!localStorageHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(localStorageHandle).forEach((key) => {
|
||||
if (key === EDGE_GATEWAY_WORKSPACE_CACHE_KEY || key.startsWith(`${EDGE_GATEWAY_WORKSPACE_CACHE_KEY}.`)) {
|
||||
localStorageHandle.removeItem(key);
|
||||
}
|
||||
});
|
||||
localStorageHandle.removeItem(storageKey);
|
||||
};
|
||||
|
||||
export const peekCachedEdgeGatewayDepartments = () => {
|
||||
const departments = loadWorkspaceCache().departments?.data;
|
||||
return Array.isArray(departments) ? cloneJson(departments) : null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @vitest-environment jsdom
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -10,14 +11,19 @@ vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
||||
|
||||
import {
|
||||
EDGE_GATEWAY_WORKSPACE_CACHE_KEY,
|
||||
clearEdgeGatewayWorkspaceCache,
|
||||
getEdgeGateway,
|
||||
getEdgeGatewayInstallTokenStatus,
|
||||
getEdgeGatewayModuleConfig,
|
||||
peekCachedEdgeGateway,
|
||||
setEdgeGatewayModuleConfig,
|
||||
} from "@/services/edgeGateways.js";
|
||||
|
||||
describe("edge gateway service", () => {
|
||||
beforeEach(() => {
|
||||
authenticatedRequestMock.mockReset();
|
||||
window.localStorage.clear();
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
});
|
||||
|
||||
it("loads module config entries and derives a keyed config object", async () => {
|
||||
@@ -76,6 +82,22 @@ describe("edge gateway service", () => {
|
||||
expect(authenticatedRequestMock).toHaveBeenCalledWith("/edge-gateways/install-token/9001/status", "GET", {});
|
||||
});
|
||||
|
||||
it("scopes workspace snapshots to the active session principal", async () => {
|
||||
window.localStorage.setItem("token", "user-one-token");
|
||||
authenticatedRequestMock.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: { id: 101, label: "User One Gateway" },
|
||||
},
|
||||
});
|
||||
|
||||
await getEdgeGateway(101);
|
||||
expect(peekCachedEdgeGateway(101)?.label).toBe("User One Gateway");
|
||||
|
||||
window.localStorage.setItem("token", "user-two-token");
|
||||
expect(peekCachedEdgeGateway(101)).toBeNull();
|
||||
expect(window.localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a stable browser cache namespace for workspace snapshots", () => {
|
||||
const source = readFileSync(join(process.cwd(), "src/services/edgeGateways.js"), "utf8");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user