This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:23 +07:00
parent d9f159a8c3
commit cda5d21d0d
53 changed files with 2711 additions and 162 deletions
+63 -1
View File
@@ -34,9 +34,71 @@ export function getServerApi() {
* and then break the moment customer sign-in lands in M8, which is precisely
* the kind of latent inconsistency worth removing now.
*/
/**
* In-memory access token.
*
* Module scope rather than React state, for the same reasons as the admin's:
* the API client reads it from inside a `fetch` callback and it must survive
* re-renders. It is never written to localStorage or a readable cookie —
* those outlive the tab and are readable by any script, which is exactly what
* an XSS payload goes looking for. The httpOnly refresh cookie is what
* survives a reload.
*/
let accessToken: string | null = null;
let onSessionLost: (() => void) | null = null;
/**
* The in-flight refresh, if any.
*
* Without it, every request that 401s at the same moment starts its own
* rotation — and because rotation invalidates the previous token, the second
* one is treated as *token reuse* and revokes the whole family. A shopper with
* two tabs open would be signed out for it. One shared promise, one rotation.
*/
let refreshInFlight: Promise<boolean> | null = null;
export const customerTokenStore = {
get: (): string | null => accessToken,
set: (token: string | null): void => {
accessToken = token;
},
onLost: (handler: (() => void) | null): void => {
onSessionLost = handler;
},
};
export const browserApi = createApiClient({
baseUrl: '',
getAccessToken: () => null, // wired to the customer auth store in M8
getAccessToken: () => customerTokenStore.get(),
/**
* Transparent re-auth: on a 401, rotate once and retry.
*
* Storefront-specific consequence: a shopper whose access token expires
* mid-checkout must not be bounced to a login form. If the rotation itself
* fails they were genuinely signed out, and the provider reacts.
*/
onUnauthorized: () => {
refreshInFlight ??= (async () => {
try {
const refreshed = await browserApi.auth.refresh();
customerTokenStore.set(refreshed.accessToken);
return true;
} catch {
customerTokenStore.set(null);
onSessionLost?.();
return false;
} finally {
// Cleared in a microtask so every caller awaiting this rotation sees
// the same result before a new one can start.
queueMicrotask(() => {
refreshInFlight = null;
});
}
})();
return refreshInFlight;
},
});
/**