Harden edge gateway workspace cache
This commit is contained in:
@@ -10,6 +10,7 @@ import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue"
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
|
||||
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
|
||||
const employees = ref([]);
|
||||
const { t } = useI18n();
|
||||
@@ -52,6 +53,7 @@ const login = async () => {
|
||||
}
|
||||
|
||||
// Save the token in the local storage
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', data.token);
|
||||
// Redirect to the dashboard
|
||||
window.location.href = '/admin';
|
||||
@@ -72,6 +74,7 @@ const loginWithPasskey = async () => {
|
||||
const result = await authenticateWithPasskey('employee', null, recaptchaToken);
|
||||
|
||||
if (result.token) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', result.token);
|
||||
window.location.href = '/admin';
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { parseError, clearErrors, addError, getError } from "@/components/reques
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
|
||||
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -68,6 +69,7 @@ const login = async () => {
|
||||
}
|
||||
|
||||
// Save the token in the local storage
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', data.token);
|
||||
// Set the success message
|
||||
successMessage.value = "Du er nu logget ind!"
|
||||
@@ -103,6 +105,7 @@ const loginWithPasskey = async () => {
|
||||
const result = await authenticateWithPasskey('user', customerNum, recaptchaToken);
|
||||
console.log('Passkey authentication result:', result);
|
||||
if (result.token) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', result.token);
|
||||
successMessage.value = "Du er nu logget ind!";
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
|
||||
import { getSubuserPasswordPolicyError } from "@/services/subuserPasswordPolicy.js";
|
||||
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -69,6 +70,7 @@ const login = async () => {
|
||||
|
||||
// Save the session token
|
||||
if (data.session) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', data.session);
|
||||
localStorage.setItem('is_subuser', 'true');
|
||||
window.location.reload();
|
||||
@@ -97,6 +99,7 @@ const loginWithPasskey = async () => {
|
||||
// Subuser login returns 'session' token, user login returns 'token'
|
||||
const sessionToken = result.session || result.token;
|
||||
if (sessionToken) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', sessionToken);
|
||||
localStorage.setItem('is_subuser', 'true');
|
||||
// Reload the page to update the UI
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { verify2FA } from "@/services/TwoFactorAuthService.js";
|
||||
import { parseError, getError, clearErrors } from "@/components/request/HandleGlobalError.vue";
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -43,9 +44,11 @@ const verify = async () => {
|
||||
|
||||
// Handle the response based on user type
|
||||
if (props.userType === 'subuser' && result.session) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', result.session);
|
||||
localStorage.setItem('is_subuser', 'true');
|
||||
} else if (result.token) {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem('token', result.token);
|
||||
localStorage.removeItem('is_subuser');
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import {API_URL} from "@/config.js";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
|
||||
export const currentDepartment = ref(null);
|
||||
export const selectDepartment = (department) => {
|
||||
@@ -31,6 +32,7 @@ export const exitSession = () => {
|
||||
}
|
||||
});
|
||||
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.removeItem('token');
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
@@ -685,6 +687,7 @@ export const SessionUser = {
|
||||
* @param {number|null} customerNumber - The billing_customer_number to select, or null to clear
|
||||
*/
|
||||
selectGrant: (customerNumber) => {
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
if (customerNumber === null) {
|
||||
localStorage.removeItem("selected_customer_number");
|
||||
SessionUser.subuser.selectedGrantCustomerNumber.value = null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import { parseError } from "@/components/request/HandleGlobalError.vue";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
import { DatabaseSystemObject } from "@/components/session/token/superUser/systemDatabase.vue";
|
||||
import { Cron } from "@/components/session/token/superUser/cron.vue";
|
||||
import { Economic } from "@/components/session/token/superUser/modules/Economic/Economic.vue";
|
||||
@@ -106,6 +107,7 @@ export const SuperUserObject = {
|
||||
const currentToken = localStorage.getItem("token");
|
||||
localStorage.setItem("superuser_token", currentToken);
|
||||
// Set the token in the local storage
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem("token", response.data.data.token);
|
||||
// Redirect the user to the dashboard
|
||||
window.location = "/";
|
||||
@@ -143,6 +145,7 @@ export const SuperUserObject = {
|
||||
// Get the superuser token
|
||||
const superuserToken = localStorage.getItem("superuser_token");
|
||||
// Set the token in the local storage
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem("token", superuserToken);
|
||||
// Clear the superuser token
|
||||
localStorage.removeItem("superuser_token");
|
||||
|
||||
+146
-28
@@ -1,6 +1,7 @@
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
|
||||
export const EDGE_GATEWAY_WORKSPACE_CACHE_KEY = "truckwash.edgeGatewayWorkspace.cache.v1";
|
||||
export const EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS = 15 * 60 * 1000;
|
||||
|
||||
const EDGE_GATEWAY_BASE = "/edge-gateways";
|
||||
const EDGE_GATEWAY_CONFIG_BASE = "/edgegateway/config";
|
||||
@@ -91,53 +92,167 @@ const toConfigPayload = (entries) => {
|
||||
return payload;
|
||||
};
|
||||
|
||||
const createEmptyWorkspaceCache = () => ({
|
||||
const createEmptyWorkspaceCache = (scope = null) => ({
|
||||
scope,
|
||||
cachedAt: Date.now(),
|
||||
departments: null,
|
||||
lists: {},
|
||||
details: {},
|
||||
});
|
||||
|
||||
const storage = () => {
|
||||
if (typeof window === "undefined" || !window.sessionStorage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.sessionStorage;
|
||||
};
|
||||
|
||||
const removeLegacyWorkspaceCache = () => {
|
||||
if (typeof window === "undefined" || !window.localStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
} catch (_error) {
|
||||
// Browser storage can be unavailable in private browsing or tests.
|
||||
}
|
||||
};
|
||||
|
||||
const getStoredValue = (key) => {
|
||||
if (typeof window === "undefined" || !window.localStorage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.localStorage;
|
||||
try {
|
||||
return window.localStorage.getItem(key);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const hashScopePart = (value) => {
|
||||
let hash = 0;
|
||||
const input = String(value || "");
|
||||
for (let index = 0; index < input.length; index += 1) {
|
||||
hash = (hash << 5) - hash + input.charCodeAt(index);
|
||||
hash |= 0;
|
||||
}
|
||||
return hash.toString(36);
|
||||
};
|
||||
|
||||
const currentWorkspaceCacheScope = () => {
|
||||
const token = getStoredValue("token");
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
token: hashScopePart(token),
|
||||
isSubuser: getStoredValue("is_subuser") === "true",
|
||||
selectedCustomerNumber: getStoredValue("selected_customer_number") || null,
|
||||
hasSuperuserToken: Boolean(getStoredValue("superuser_token")),
|
||||
});
|
||||
};
|
||||
|
||||
const isFreshCachedAt = (cachedAt) => Date.now() - Number(cachedAt || 0) <= EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS;
|
||||
const isFreshCacheEntry = (entry) => Boolean(entry && typeof entry === "object" && isFreshCachedAt(entry.cachedAt));
|
||||
const isSensitiveCacheField = (fieldName) =>
|
||||
/(^|[_-])(token|secret|password|credential|authorization|api[_-]?key|private[_-]?key)($|[_-])/i.test(
|
||||
String(fieldName || "")
|
||||
) || String(fieldName || "").toLowerCase() === "config_json";
|
||||
|
||||
const sanitizeCacheValue = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => sanitizeCacheValue(item));
|
||||
}
|
||||
|
||||
if (!value || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return Object.entries(value).reduce((sanitized, [key, childValue]) => {
|
||||
if (!isSensitiveCacheField(key)) {
|
||||
sanitized[key] = sanitizeCacheValue(childValue);
|
||||
}
|
||||
return sanitized;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const sanitizeGatewayCachePayload = (value) => cloneJson(sanitizeCacheValue(value));
|
||||
|
||||
const loadWorkspaceCache = () => {
|
||||
if (edgeGatewayWorkspaceCache !== null) {
|
||||
removeLegacyWorkspaceCache();
|
||||
const scope = currentWorkspaceCacheScope();
|
||||
if (!scope) {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(null);
|
||||
return edgeGatewayWorkspaceCache;
|
||||
}
|
||||
|
||||
const localStorageHandle = storage();
|
||||
if (!localStorageHandle) {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
|
||||
if (
|
||||
edgeGatewayWorkspaceCache !== null &&
|
||||
edgeGatewayWorkspaceCache.scope === scope &&
|
||||
isFreshCachedAt(edgeGatewayWorkspaceCache.cachedAt)
|
||||
) {
|
||||
return edgeGatewayWorkspaceCache;
|
||||
}
|
||||
|
||||
const storageHandle = storage();
|
||||
if (!storageHandle) {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(scope);
|
||||
return edgeGatewayWorkspaceCache;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = JSON.parse(localStorageHandle.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY) || "null");
|
||||
const decoded = JSON.parse(storageHandle.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY) || "null");
|
||||
if (!decoded || typeof decoded !== "object" || decoded.scope !== scope || !isFreshCachedAt(decoded.cachedAt)) {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(scope);
|
||||
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
return edgeGatewayWorkspaceCache;
|
||||
}
|
||||
|
||||
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 : {},
|
||||
...createEmptyWorkspaceCache(scope),
|
||||
cachedAt: Number(decoded.cachedAt || Date.now()),
|
||||
departments: isFreshCacheEntry(decoded.departments) ? decoded.departments : null,
|
||||
lists:
|
||||
decoded?.lists && typeof decoded.lists === "object"
|
||||
? Object.fromEntries(Object.entries(decoded.lists).filter(([, entry]) => isFreshCacheEntry(entry)))
|
||||
: {},
|
||||
details:
|
||||
decoded?.details && typeof decoded.details === "object"
|
||||
? Object.fromEntries(Object.entries(decoded.details).filter(([, entry]) => isFreshCacheEntry(entry)))
|
||||
: {},
|
||||
};
|
||||
} catch (_error) {
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
|
||||
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(scope);
|
||||
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
}
|
||||
|
||||
return edgeGatewayWorkspaceCache;
|
||||
};
|
||||
|
||||
const persistWorkspaceCache = () => {
|
||||
const localStorageHandle = storage();
|
||||
if (!localStorageHandle || edgeGatewayWorkspaceCache === null) {
|
||||
const storageHandle = storage();
|
||||
const scope = currentWorkspaceCacheScope();
|
||||
if (!storageHandle || edgeGatewayWorkspaceCache === null || !scope || edgeGatewayWorkspaceCache.scope !== scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorageHandle.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(edgeGatewayWorkspaceCache));
|
||||
edgeGatewayWorkspaceCache.cachedAt = Date.now();
|
||||
storageHandle.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(edgeGatewayWorkspaceCache));
|
||||
};
|
||||
|
||||
export const clearEdgeGatewayWorkspaceCache = () => {
|
||||
edgeGatewayWorkspaceCache = null;
|
||||
removeLegacyWorkspaceCache();
|
||||
const storageHandle = storage();
|
||||
if (!storageHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
|
||||
};
|
||||
|
||||
const mergeGatewaySnapshots = (currentGateway, nextGateway) => {
|
||||
@@ -181,8 +296,9 @@ const cacheGatewaySnapshot = (gateway) => {
|
||||
}
|
||||
|
||||
const cache = loadWorkspaceCache();
|
||||
const gatewayId = String(gateway.id);
|
||||
const mergedGateway = mergeGatewaySnapshots(cache.details[gatewayId]?.data || null, gateway);
|
||||
const sanitizedGateway = sanitizeGatewayCachePayload(gateway);
|
||||
const gatewayId = String(sanitizedGateway.id);
|
||||
const mergedGateway = mergeGatewaySnapshots(cache.details[gatewayId]?.data || null, sanitizedGateway);
|
||||
cache.details[gatewayId] = {
|
||||
cachedAt: Date.now(),
|
||||
data: mergedGateway,
|
||||
@@ -198,9 +314,9 @@ const cacheGatewaySnapshot = (gateway) => {
|
||||
const existingIndex = rows.findIndex((item) => Number(item?.id || 0) === Number(gateway.id));
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
rows[existingIndex] = mergeGatewaySnapshots(rows[existingIndex], gateway);
|
||||
rows[existingIndex] = mergeGatewaySnapshots(rows[existingIndex], sanitizedGateway);
|
||||
} else if (matchesDepartment) {
|
||||
rows.unshift(cloneJson(gateway));
|
||||
rows.unshift(cloneJson(sanitizedGateway));
|
||||
}
|
||||
|
||||
cache.lists[cacheKey] = {
|
||||
@@ -216,18 +332,19 @@ const cacheGatewaySnapshot = (gateway) => {
|
||||
|
||||
const cacheFleetSnapshot = ({ departmentId = null, view = "summary", rows = [], meta = {} } = {}) => {
|
||||
const cache = loadWorkspaceCache();
|
||||
const sanitizedRows = sanitizeGatewayCachePayload(Array.isArray(rows) ? rows : []);
|
||||
const key = listCacheKey({ departmentId, view });
|
||||
cache.lists[key] = {
|
||||
params: {
|
||||
departmentId: normalizeDepartmentId(departmentId),
|
||||
view: String(view || "summary"),
|
||||
},
|
||||
rows: cloneJson(Array.isArray(rows) ? rows : []),
|
||||
meta: cloneJson(meta || {}),
|
||||
rows: sanitizedRows,
|
||||
meta: sanitizeGatewayCachePayload(meta || {}),
|
||||
cachedAt: Date.now(),
|
||||
};
|
||||
|
||||
rows.forEach((gateway) => {
|
||||
sanitizedRows.forEach((gateway) => {
|
||||
if (!gateway?.id) {
|
||||
return;
|
||||
}
|
||||
@@ -236,7 +353,7 @@ const cacheFleetSnapshot = ({ departmentId = null, view = "summary", rows = [],
|
||||
if (!cache.details[gatewayId]) {
|
||||
cache.details[gatewayId] = {
|
||||
cachedAt: Date.now(),
|
||||
data: cloneJson(gateway),
|
||||
data: sanitizeGatewayCachePayload(gateway),
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -254,7 +371,7 @@ const cacheDepartmentsSnapshot = (departments) => {
|
||||
const cache = loadWorkspaceCache();
|
||||
cache.departments = {
|
||||
cachedAt: Date.now(),
|
||||
data: cloneJson(Array.isArray(departments) ? departments : []),
|
||||
data: sanitizeGatewayCachePayload(Array.isArray(departments) ? departments : []),
|
||||
};
|
||||
persistWorkspaceCache();
|
||||
};
|
||||
@@ -296,13 +413,14 @@ export const unwrapEdgeGatewayResponse = (response, fallback = null) => response
|
||||
export const unwrapEdgeGatewayMeta = (response) => response?.data?.meta ?? {};
|
||||
|
||||
export const peekCachedEdgeGatewayDepartments = () => {
|
||||
const departments = loadWorkspaceCache().departments?.data;
|
||||
return Array.isArray(departments) ? cloneJson(departments) : null;
|
||||
const entry = loadWorkspaceCache().departments;
|
||||
const departments = entry?.data;
|
||||
return isFreshCacheEntry(entry) && Array.isArray(departments) ? cloneJson(departments) : null;
|
||||
};
|
||||
|
||||
export const peekCachedEdgeGatewayList = ({ departmentId = null, view = "summary" } = {}) => {
|
||||
const entry = loadWorkspaceCache().lists[listCacheKey({ departmentId, view })];
|
||||
if (!entry || !Array.isArray(entry.rows)) {
|
||||
if (!isFreshCacheEntry(entry) || !Array.isArray(entry.rows)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -320,7 +438,7 @@ export const peekCachedEdgeGateway = (gatewayId) => {
|
||||
}
|
||||
|
||||
const entry = loadWorkspaceCache().details[normalizedGatewayId];
|
||||
return entry?.data ? cloneJson(entry.data) : null;
|
||||
return isFreshCacheEntry(entry) && entry?.data ? cloneJson(entry.data) : null;
|
||||
};
|
||||
|
||||
export const patchEdgeGatewayCache = (gatewayId, patch) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { QrcodeStream, QrcodeDropZone, QrcodeCapture } from 'vue-qrcode-reader'
|
||||
import { sounds } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
import PageLoader from "@/components/global/PageLoader.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
|
||||
const isProcessingQRCode = ref(false); // State to indicate if QR code is being processed
|
||||
const playCaptureSound = () => {
|
||||
@@ -20,6 +21,7 @@ const playCaptureSound = () => {
|
||||
|
||||
const applyToken = (token: string) => {
|
||||
isProcessingQRCode.value = true; // Start processing
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem("token", token);
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||
import PageLoader from "@/components/global/PageLoader.vue";
|
||||
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
|
||||
|
||||
const isProcessing = ref(false);
|
||||
const hasToken = computed(() => localStorage.getItem("superuser_token") !== null);
|
||||
|
||||
const applyToken = (token: string) => {
|
||||
isProcessing.value = true; // Start processing
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.removeItem("superuser_token");
|
||||
window.location.href = "/";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -10,7 +12,12 @@ vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
||||
|
||||
import {
|
||||
EDGE_GATEWAY_WORKSPACE_CACHE_KEY,
|
||||
EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS,
|
||||
clearEdgeGatewayWorkspaceCache,
|
||||
getEdgeGatewayInstallTokenStatus,
|
||||
listEdgeGateways,
|
||||
peekCachedEdgeGateway,
|
||||
peekCachedEdgeGatewayList,
|
||||
getEdgeGatewayModuleConfig,
|
||||
setEdgeGatewayModuleConfig,
|
||||
} from "@/services/edgeGateways.js";
|
||||
@@ -18,6 +25,9 @@ import {
|
||||
describe("edge gateway service", () => {
|
||||
beforeEach(() => {
|
||||
authenticatedRequestMock.mockReset();
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
clearEdgeGatewayWorkspaceCache();
|
||||
});
|
||||
|
||||
it("loads module config entries and derives a keyed config object", async () => {
|
||||
@@ -76,11 +86,77 @@ describe("edge gateway service", () => {
|
||||
expect(authenticatedRequestMock).toHaveBeenCalledWith("/edge-gateways/install-token/9001/status", "GET", {});
|
||||
});
|
||||
|
||||
it("keeps a stable browser cache namespace for workspace snapshots", () => {
|
||||
it("uses a stable browser cache namespace for session-scoped workspace snapshots", () => {
|
||||
const source = readFileSync(join(process.cwd(), "src/services/edgeGateways.js"), "utf8");
|
||||
|
||||
expect(EDGE_GATEWAY_WORKSPACE_CACHE_KEY).toBe("truckwash.edgeGatewayWorkspace.cache.v1");
|
||||
expect(source).toContain("localStorage");
|
||||
expect(source).toContain("sessionStorage");
|
||||
expect(source).toContain("EDGE_GATEWAY_WORKSPACE_CACHE_KEY");
|
||||
});
|
||||
|
||||
it("stores workspace cache entries in session storage scoped to the active token", async () => {
|
||||
localStorage.setItem("token", "operator-a-token");
|
||||
authenticatedRequestMock.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: 101,
|
||||
department_id: 55,
|
||||
name: "Privileged gateway",
|
||||
metadata: { agent_token: "secret-agent-token", health: "ok" },
|
||||
bindings: [{ relay_id: 7, credential_secret: "binding-secret" }],
|
||||
},
|
||||
],
|
||||
meta: { fleet_usage: { total: 1 }, api_key: "fleet-api-key" },
|
||||
},
|
||||
});
|
||||
|
||||
await listEdgeGateways({ departmentId: 55, view: "summary" });
|
||||
|
||||
expect(localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
||||
const persisted = JSON.parse(sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY));
|
||||
expect(persisted.scope).toEqual(expect.any(String));
|
||||
expect(persisted.scope).not.toContain("operator-a-token");
|
||||
expect(persisted.lists).toBeTruthy();
|
||||
expect(JSON.stringify(persisted)).not.toContain("secret-agent-token");
|
||||
expect(JSON.stringify(persisted)).not.toContain("binding-secret");
|
||||
expect(JSON.stringify(persisted)).not.toContain("fleet-api-key");
|
||||
expect(peekCachedEdgeGateway(101)?.metadata?.health).toBe("ok");
|
||||
|
||||
localStorage.setItem("token", "operator-b-token");
|
||||
|
||||
expect(peekCachedEdgeGatewayList({ departmentId: 55, view: "summary" })).toBeNull();
|
||||
expect(peekCachedEdgeGateway(101)).toBeNull();
|
||||
});
|
||||
|
||||
it("expires stale workspace cache entries", async () => {
|
||||
localStorage.setItem("token", "operator-a-token");
|
||||
authenticatedRequestMock.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: [{ id: 202, department_id: 55, name: "Stale gateway" }],
|
||||
meta: {},
|
||||
},
|
||||
});
|
||||
|
||||
await listEdgeGateways({ departmentId: 55, view: "summary" });
|
||||
|
||||
const persisted = JSON.parse(sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY));
|
||||
persisted.cachedAt = Date.now() - EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS - 1;
|
||||
Object.values(persisted.lists).forEach((entry) => {
|
||||
entry.cachedAt = persisted.cachedAt;
|
||||
});
|
||||
Object.values(persisted.details).forEach((entry) => {
|
||||
entry.cachedAt = persisted.cachedAt;
|
||||
});
|
||||
sessionStorage.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(persisted));
|
||||
vi.resetModules();
|
||||
|
||||
const { peekCachedEdgeGateway: peekGateway, peekCachedEdgeGatewayList: peekList } = await import(
|
||||
"@/services/edgeGateways.js"
|
||||
);
|
||||
|
||||
expect(peekList({ departmentId: 55, view: "summary" })).toBeNull();
|
||||
expect(peekGateway(202)).toBeNull();
|
||||
expect(sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ vi.mock("sweetalert2", () => ({
|
||||
}));
|
||||
|
||||
import { getSessionData, SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { EDGE_GATEWAY_WORKSPACE_CACHE_KEY } from "@/services/edgeGateways.js";
|
||||
import {
|
||||
buildPeriodCacheKey,
|
||||
getCachedPeriodPage,
|
||||
@@ -91,9 +92,13 @@ describe("session bootstrap failure handling", () => {
|
||||
order_items: [{ license_plate: "PII-123" }],
|
||||
potential_duplicates: [],
|
||||
});
|
||||
window.sessionStorage.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify({ details: { 1: "gateway" } }));
|
||||
window.localStorage.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify({ details: { 1: "legacy" } }));
|
||||
|
||||
expect(getCachedPeriodPage(periodCacheKey)?.data?.types?.all?.[0]?.customer_name).toBe("Sensitive Customer");
|
||||
expect(getCachedXlvaskUsageAmount(8101)?.order_items?.[0]?.license_plate).toBe("PII-123");
|
||||
expect(window.sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toContain("gateway");
|
||||
expect(window.localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toContain("legacy");
|
||||
|
||||
SessionUser.auth.forceClearSession();
|
||||
|
||||
@@ -101,6 +106,8 @@ describe("session bootstrap failure handling", () => {
|
||||
expect(getCachedXlvaskUsageAmount(8101)).toBeNull();
|
||||
expect(Object.keys(window.sessionStorage).filter((key) => key.startsWith("invoicing-period-page:")).length).toBe(0);
|
||||
expect(Object.keys(window.sessionStorage).filter((key) => key.startsWith("xlvask-usage-amount:")).length).toBe(0);
|
||||
expect(window.sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
||||
expect(window.localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it("clears an invalid stored session after ping confirms the API is reachable", async () => {
|
||||
|
||||
Reference in New Issue
Block a user