Files
edr-platform/apps/edr-freight-web/backoffice/src/auth/refreshScheduler.ts

83 lines
2.4 KiB
TypeScript

import { isAxiosError } from "axios";
import {
REFRESH_TOKEN_COOKIE,
clearSessionCookies,
getCookie,
} from "./cookies";
import { refreshSessionTokens } from "./http";
/**
* Proactively refreshes the token pair on a fixed cadence so the server-side
* session (a sliding 1-hour window, extended only by /auth/refresh-token) is
* kept alive while the app is open. The 401 interceptor in http.ts remains
* the reactive fallback; both share the same single-flight refresh call.
*
* The interval MUST stay well under the server session window (60 min).
*/
const DEFAULT_INTERVAL_MINUTES = 10;
const getIntervalMs = () => {
const minutes = Number(import.meta.env.VITE_TOKEN_REFRESH_INTERVAL_MINUTES);
return (
(Number.isFinite(minutes) && minutes > 0
? minutes
: DEFAULT_INTERVAL_MINUTES) * 60_000
);
};
let timerId: number | null = null;
let lastRefreshAt = 0;
const refreshNow = async () => {
if (!getCookie(REFRESH_TOKEN_COOKIE)) {
// Logged out elsewhere; nothing to keep alive.
stopTokenRefreshScheduler();
return;
}
try {
await refreshSessionTokens();
lastRefreshAt = Date.now();
} catch (error) {
// Network hiccups are retried on the next tick; only an explicit server
// rejection means the session is dead.
if (isAxiosError(error) && error.response) {
stopTokenRefreshScheduler();
clearSessionCookies();
window.location.replace("/auth");
}
}
};
/**
* Browsers freeze timers in background tabs — a tab waking up past its
* refresh deadline refreshes immediately instead of waiting a full interval.
*/
const onVisibilityChange = () => {
if (document.visibilityState !== "visible") return;
if (Date.now() - lastRefreshAt >= getIntervalMs()) {
void refreshNow();
}
};
export const startTokenRefreshScheduler = () => {
stopTokenRefreshScheduler();
// Token age is unknown here (fresh login vs. hours-old page reload), so
// refresh right away to extend the session window from "now".
lastRefreshAt = 0;
void refreshNow();
timerId = window.setInterval(() => void refreshNow(), getIntervalMs());
document.addEventListener("visibilitychange", onVisibilityChange);
};
export const stopTokenRefreshScheduler = () => {
if (timerId !== null) {
window.clearInterval(timerId);
timerId = null;
}
document.removeEventListener("visibilitychange", onVisibilityChange);
};