Files
pleno-vue/src/components/session/authenticatedRequest.vue
T

372 lines
11 KiB
Vue

<script>
import axios from 'axios'
import { enqueueRequest } from "@/services/requestQueue.js";
import { buildCurrentReleaseHeaders, resolveReleaseApiUrl } from "@/services/releaseTimeline.js";
import { isTrustedReleaseUrl } from "@/services/releaseTrust.js";
/**
* Get the selected customer number for X-Customer-Number header (used by subusers)
* @returns {number|null}
*/
const getSelectedCustomerNumber = () => {
const stored = localStorage.getItem('selected_customer_number');
return stored ? parseInt(stored) : null;
};
const MY_ACTIVE_WASH_ENDPOINT = '/modules/self-serve/lane/wash/my-active-wash';
const ACTIVE_WASH_STARTED_STATUSES = new Set(['MACHINE_RELAY_ENABLED', 'MACHINE_STARTED']);
const SELF_SERVE_HARDWARE_QUEUE_GROUP = 'SELF_SERVE_HARDWARE';
const POS_SCANNER_QUEUE_GROUP = 'POS_SCANNER';
const POS_STRIPE_QUEUE_GROUP = 'POS_STRIPE';
const FETCH_TRANSPORT = 'fetch';
const SELF_SERVE_HARDWARE_ENDPOINTS = [
'/modules/self-serve/lane/command',
'/modules/self-serve/lane/relay/',
'/modules/self-serve/lane/gate/open',
'/modules/self-serve/lane/force/machine',
];
const POS_LATENCY_QUEUE_RULES = [
{
endpoints: ['/modules/scanner/lpr'],
queueGroup: POS_SCANNER_QUEUE_GROUP,
concurrencyLimit: 1,
retryByStatusCode: {},
skipRequestByteAccounting: true,
skipResponseByteAccounting: true,
skipNetworkTotals: true,
insightKey: 'scanner',
recordRecentOnSuccess: false,
trackActiveRequest: false,
trackProgressCounters: false,
},
{
endpoints: ['/modules/stripe/invoice'],
queueGroup: POS_STRIPE_QUEUE_GROUP,
concurrencyLimit: 2,
retryByStatusCode: {},
},
];
const normalizeStatus = (status) => String(status || '').trim().toUpperCase();
const parseDateTimeMs = (value) => {
if (!value) {
return null;
}
const timestamp = Date.parse(String(value));
return Number.isFinite(timestamp) ? timestamp : null;
};
const hasActiveWashStartEvidence = (details) => {
const session = details?.session || {};
const status = normalizeStatus(session?.status ?? details?.status);
return (
ACTIVE_WASH_STARTED_STATUSES.has(status) ||
session?.machine_relay_enabled === true ||
session?.machine_start_triggered === true ||
details?.machine_relay_enabled === true ||
details?.machine_start_triggered === true ||
parseDateTimeMs(session?.wash_started_at) !== null ||
parseDateTimeMs(session?.machine_start_triggered_at) !== null ||
parseDateTimeMs(session?.machine_relay_enabled_at) !== null
);
};
const normalizeActiveWashResponse = (url, method, response) => {
if (String(method || '').toUpperCase() !== 'GET' || !String(url || '').includes(MY_ACTIVE_WASH_ENDPOINT)) {
return response;
}
const details = response?.data?.data;
if (!details?.in_progress || hasActiveWashStartEvidence(details)) {
return response;
}
return {
...response,
data: {
...response.data,
data: {
...details,
in_progress: false,
},
},
};
};
const isSelfServeHardwareMutation = (url, method) => {
const normalizedMethod = String(method || '').trim().toUpperCase();
if (normalizedMethod === 'GET') {
return false;
}
const normalizedUrl = String(url || '');
return SELF_SERVE_HARDWARE_ENDPOINTS.some((endpoint) => normalizedUrl.includes(endpoint));
};
const findPosLatencyQueueRule = (url, method) => {
const normalizedMethod = String(method || '').trim().toUpperCase();
if (normalizedMethod === 'GET') {
return null;
}
const normalizedUrl = String(url || '');
return POS_LATENCY_QUEUE_RULES.find((rule) =>
rule.endpoints.some((endpoint) => normalizedUrl.includes(endpoint))
) || null;
};
const hasHeader = (headers, name) => {
const normalizedName = String(name || '').trim().toLowerCase();
if (!normalizedName || !headers || typeof headers !== 'object') {
return false;
}
return Object.keys(headers).some((headerName) => String(headerName).toLowerCase() === normalizedName);
};
const parseFetchResponseData = async (response) => {
const text = await response.text();
if (!text) {
return null;
}
const contentType = response.headers?.get?.('content-type') || '';
if (
contentType.toLowerCase().includes('application/json') ||
text.trim().startsWith('{') ||
text.trim().startsWith('[')
) {
try {
return JSON.parse(text);
} catch (_error) {
return text;
}
}
return text;
};
const buildFetchBody = (method, data, headers) => {
if (String(method || '').trim().toUpperCase() === 'GET' || data === undefined || data === null) {
return undefined;
}
if (
typeof Blob !== 'undefined' && data instanceof Blob ||
typeof FormData !== 'undefined' && data instanceof FormData ||
typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams ||
typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer ||
typeof ReadableStream !== 'undefined' && data instanceof ReadableStream ||
typeof data === 'string'
) {
return data;
}
if (!hasHeader(headers, 'Content-Type')) {
headers['Content-Type'] = 'application/json';
}
return JSON.stringify(data);
};
const executeFetchRequest = async ({ method, url, data, signal, headers }) => {
const fetchHeaders = { ...headers };
const body = buildFetchBody(method, data, fetchHeaders);
const response = await fetch(url, {
method,
headers: fetchHeaders,
...(body !== undefined ? { body } : {}),
...(signal ? { signal } : {}),
});
const responseData = await parseFetchResponseData(response);
const axiosLikeResponse = {
data: responseData,
status: response.status,
statusText: response.statusText,
headers: response.headers,
config: {
data,
headers: fetchHeaders,
method,
url,
},
request: null,
};
if (response.ok) {
return axiosLikeResponse;
}
const error = new Error(`Request failed with status code ${response.status}`);
error.name = 'AxiosError';
error.response = axiosLikeResponse;
throw error;
};
const buildRequestQueueOptions = (url, method, options = {}) => {
const queueOptions = {
retryByStatusCode: options?.retryByStatusCode,
shouldRetry: options?.shouldRetry,
queueGroup: options?.queueGroup,
concurrencyLimit: options?.concurrencyLimit,
skipRequestByteAccounting: options?.skipRequestByteAccounting,
skipResponseByteAccounting: options?.skipResponseByteAccounting,
skipNetworkTotals: options?.skipNetworkTotals,
insightKey: options?.insightKey,
recordRecentOnSuccess: options?.recordRecentOnSuccess,
trackActiveRequest: options?.trackActiveRequest,
trackProgressCounters: options?.trackProgressCounters,
};
if (isSelfServeHardwareMutation(url, method)) {
queueOptions.retryByStatusCode ??= {};
queueOptions.queueGroup ??= SELF_SERVE_HARDWARE_QUEUE_GROUP;
queueOptions.concurrencyLimit ??= 1;
}
const posQueueRule = findPosLatencyQueueRule(url, method);
if (posQueueRule) {
queueOptions.retryByStatusCode ??= posQueueRule.retryByStatusCode;
queueOptions.queueGroup ??= posQueueRule.queueGroup;
queueOptions.concurrencyLimit ??= posQueueRule.concurrencyLimit;
queueOptions.skipRequestByteAccounting ??= posQueueRule.skipRequestByteAccounting;
queueOptions.skipResponseByteAccounting ??= posQueueRule.skipResponseByteAccounting;
queueOptions.skipNetworkTotals ??= posQueueRule.skipNetworkTotals;
queueOptions.insightKey ??= posQueueRule.insightKey;
queueOptions.recordRecentOnSuccess ??= posQueueRule.recordRecentOnSuccess;
queueOptions.trackActiveRequest ??= posQueueRule.trackActiveRequest;
queueOptions.trackProgressCounters ??= posQueueRule.trackProgressCounters;
}
return queueOptions;
};
export const authenticatedRequest = (url, method, data, catchCallable = null, thenCallable = null, options = {}) => {
const token = localStorage.getItem('token');
if (!token) {
//throw new Error('No token was found, unable to make authenticated request');
}
const requestUrl = resolveReleaseApiUrl(url);
const canSendCredentials = isTrustedReleaseUrl(requestUrl);
// Build headers
const headers = {
...buildCurrentReleaseHeaders(),
...(options?.headers || {}),
};
if (canSendCredentials && token && token.length > 0) {
headers.Authorization = `Bearer ${token}`;
}
// Add X-Customer-Number header if subuser has selected a grant
const isSubuser = localStorage.getItem('is_subuser') === 'true';
const selectedCustomerNumber = getSelectedCustomerNumber();
if (canSendCredentials && isSubuser && selectedCustomerNumber) {
headers['X-Customer-Number'] = selectedCustomerNumber;
}
const useFetchTransport = options?.transport === FETCH_TRANSPORT;
return enqueueRequest(
() => useFetchTransport
? executeFetchRequest({
method,
url: requestUrl,
data,
signal: options?.signal,
headers,
})
: axios({
method,
url: requestUrl,
...(method === 'GET' ? { params: data } : { data }),
...(options?.signal ? { signal: options.signal } : {}),
__skipRequestQueue: true,
headers
}),
{
method,
url: requestUrl,
requestData: {
params: method === 'GET' ? data : null,
data: method === 'GET' ? null : data,
headers,
},
signal: options?.signal,
...buildRequestQueueOptions(requestUrl, method, options),
}
)
.catch((error) => {
if (catchCallable) {
// Call the catch callable
catchCallable(error);
}
throw error;
})
.then((response) => {
response = normalizeActiveWashResponse(url, method, response);
if (thenCallable) {
// Call the then callable
thenCallable(response);
}
return response;
});
};
export const unauthenticatedRequest = (url, method, data, catchCallable = null, thenCallable = null) => {
return axios({
method,
url: resolveReleaseApiUrl(url),
...(method === 'GET' ? { params: data } : { data }),
headers: buildCurrentReleaseHeaders(),
}).catch((error) => {
if (catchCallable) {
// Call the catch callable
catchCallable(error);
}
throw error;
}).then((response) => {
if (thenCallable) {
// Call the then callable
thenCallable(response);
}
return response;
});
};
export const paginatedGetRequest = (url, currentPage, itemsPerPage) => {
const token = localStorage.getItem('token');
if (!token) {
return null;
}
const requestUrl = resolveReleaseApiUrl(url);
const canSendCredentials = isTrustedReleaseUrl(requestUrl);
// Build headers
const headers = {
...buildCurrentReleaseHeaders(),
};
if (canSendCredentials) {
headers.Authorization = `Bearer ${token}`;
}
// Add X-Customer-Number header if subuser has selected a grant
const isSubuser = localStorage.getItem('is_subuser') === 'true';
const selectedCustomerNumber = getSelectedCustomerNumber();
if (canSendCredentials && isSubuser && selectedCustomerNumber) {
headers['X-Customer-Number'] = selectedCustomerNumber;
}
return axios.get(requestUrl, {
params: {
page: currentPage,
limit: itemsPerPage
},
headers
});
};
</script>