mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-27 01:22:49 +00:00
feat: implement cross-tab idle session timeout and add request collapsing to token refresh logic.
This commit is contained in:
@@ -21,6 +21,17 @@ const sessionExpired = (message: string) =>
|
||||
|
||||
let inFlight: Promise<string> | null = null;
|
||||
|
||||
let signedOut = false;
|
||||
|
||||
/**
|
||||
* Set on logout, cleared on login. A refresh that was already in flight when
|
||||
* the user (or the idle timer) signed out must not write its response back
|
||||
* into storage — that would silently re-authenticate an unattended desk.
|
||||
*/
|
||||
export function setSignedOut(v: boolean) {
|
||||
signedOut = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concurrent 401s must share one refresh. A page fires several requests at
|
||||
* once; without this each one POSTs the same refresh token, the server rotates
|
||||
@@ -28,13 +39,34 @@ let inFlight: Promise<string> | null = null;
|
||||
* winner just renewed.
|
||||
*/
|
||||
export function refreshAccessToken(): Promise<string> {
|
||||
inFlight ??= runRefresh().finally(() => {
|
||||
inFlight ??= acquireAndRefresh().finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-tab guard on top of the in-tab one: cookies are shared per origin, so
|
||||
* two tabs expiring together would both POST the same rotating refresh token
|
||||
* and the loser would tear down the session the winner just renewed. A Web
|
||||
* Lock makes the second tab wait; if the first tab already refreshed while it
|
||||
* waited, the fresh token is sitting in storage and no request is needed.
|
||||
*/
|
||||
async function acquireAndRefresh(): Promise<string> {
|
||||
if (typeof navigator === "undefined" || !navigator.locks) {
|
||||
// Old Safari / test env — in-tab de-dup still applies.
|
||||
return runRefresh();
|
||||
}
|
||||
const tokenBefore = authStorage.getToken();
|
||||
return navigator.locks.request("ema-token-refresh", async () => {
|
||||
const current = authStorage.getToken();
|
||||
if (current && current !== tokenBefore) return current;
|
||||
return runRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
async function runRefresh(): Promise<string> {
|
||||
if (signedOut) throw new Error("Signed out");
|
||||
const refreshToken = authStorage.getRefreshToken();
|
||||
if (!refreshToken) throw sessionExpired("No refresh token available");
|
||||
|
||||
@@ -61,6 +93,9 @@ async function runRefresh(): Promise<string> {
|
||||
}
|
||||
|
||||
const data: RefreshResponse = await response.json();
|
||||
// Deliberately NOT sessionExpired: the user already signed out, so there is
|
||||
// no session left to end — just refuse to resurrect it.
|
||||
if (signedOut) throw new Error("Signed out during refresh");
|
||||
authStorage.setToken(data.token);
|
||||
if (data.refreshToken) authStorage.setRefreshToken(data.refreshToken);
|
||||
return data.token;
|
||||
|
||||
Reference in New Issue
Block a user