bugfix: organization not selected problem fixed. being logged out too often fixed.
This commit is contained in:
155
frontend/src/lib/auth/proactiveRefresh.ts
Normal file
155
frontend/src/lib/auth/proactiveRefresh.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { authApi } from '@/lib/api/auth';
|
||||
|
||||
const STORAGE_KEY = 'dyolink.accessTokenExpiresAt';
|
||||
/** Refresh this long before the access JWT expires. */
|
||||
const REFRESH_BUFFER_MS = 2 * 60 * 1000;
|
||||
/** Safety cap — never call /auth/refresh more than once per minute per tab. */
|
||||
const MIN_REFRESH_GAP_MS = 60 * 1000;
|
||||
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null;
|
||||
let refreshInFlight: Promise<string | undefined> | null = null;
|
||||
let lastRefreshAt = 0;
|
||||
|
||||
export function setAccessTokenExpiresAt(iso: string): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(STORAGE_KEY, iso);
|
||||
}
|
||||
|
||||
export function clearAccessTokenExpiresAt(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function rememberAccessTokenExpiresAt(
|
||||
iso: string | undefined | null,
|
||||
): void {
|
||||
if (iso) {
|
||||
setAccessTokenExpiresAt(iso);
|
||||
}
|
||||
}
|
||||
|
||||
function getAccessTokenExpiresAtMs(): number | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ms = Date.parse(raw);
|
||||
return Number.isFinite(ms) ? ms : null;
|
||||
}
|
||||
|
||||
function clearTimer(): void {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function computeDelayMs(expiresAtMs: number): number {
|
||||
const refreshAt = expiresAtMs - REFRESH_BUFFER_MS;
|
||||
const delay = refreshAt - Date.now();
|
||||
return Math.max(delay, MIN_REFRESH_GAP_MS);
|
||||
}
|
||||
|
||||
function shouldRefreshNow(expiresAtMs: number): boolean {
|
||||
return expiresAtMs - REFRESH_BUFFER_MS <= Date.now();
|
||||
}
|
||||
|
||||
async function refreshAccessTokenWithOrg(): Promise<string | undefined> {
|
||||
if (refreshInFlight) {
|
||||
return refreshInFlight;
|
||||
}
|
||||
|
||||
if (Date.now() - lastRefreshAt < MIN_REFRESH_GAP_MS) {
|
||||
const expiresAtMs = getAccessTokenExpiresAtMs();
|
||||
return expiresAtMs
|
||||
? new Date(expiresAtMs).toISOString()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
refreshInFlight = (async () => {
|
||||
try {
|
||||
const result = await authApi.refreshSessionFromCookies();
|
||||
const expiresAt = result.data?.accessTokenExpiresAt;
|
||||
rememberAccessTokenExpiresAt(expiresAt);
|
||||
|
||||
const orgId = localStorage.getItem('currentOrganizationId');
|
||||
if (orgId) {
|
||||
try {
|
||||
await authApi.selectOrganization(orgId);
|
||||
} catch {
|
||||
/* refresh may already preserve organizationId on the JWT */
|
||||
}
|
||||
}
|
||||
|
||||
lastRefreshAt = Date.now();
|
||||
return expiresAt;
|
||||
} finally {
|
||||
refreshInFlight = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return refreshInFlight;
|
||||
}
|
||||
|
||||
function scheduleNextRefresh(reschedule: () => void): void {
|
||||
clearTimer();
|
||||
|
||||
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAtMs = getAccessTokenExpiresAtMs();
|
||||
if (!expiresAtMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
timerId = setTimeout(() => {
|
||||
void refreshAccessTokenWithOrg()
|
||||
.catch(() => {
|
||||
/* reactive refresh / next visibility check will recover */
|
||||
})
|
||||
.finally(reschedule);
|
||||
}, computeDelayMs(expiresAtMs));
|
||||
}
|
||||
|
||||
/** Keeps the access cookie fresh while the tab is visible. Returns a cleanup function. */
|
||||
export function startProactiveSessionRefresh(): () => void {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const reschedule = () => scheduleNextRefresh(reschedule);
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
const expiresAtMs = getAccessTokenExpiresAtMs();
|
||||
if (expiresAtMs && shouldRefreshNow(expiresAtMs)) {
|
||||
void refreshAccessTokenWithOrg()
|
||||
.catch(() => undefined)
|
||||
.finally(reschedule);
|
||||
} else {
|
||||
reschedule();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimer();
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
reschedule();
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
clearTimer();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user