feat(auth): extend refresh TTL and keep session on transient errors
Bumps refresh token lifetime to 720h with env overrides, and stops clearing the local session for non-401 refresh failures so users aren't logged out by transient network blips. Adds a 60s retry timer when a refresh fails for non-auth reasons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,10 @@ function expireStoredSession(): void {
|
||||
clearStoredSession({ notifyAuthExpired: true });
|
||||
}
|
||||
|
||||
export function isAuthSessionExpiredError(error: unknown): boolean {
|
||||
return error instanceof ApiClientError && error.status === 401;
|
||||
}
|
||||
|
||||
export function shouldRefreshAccessToken(
|
||||
expiresAt: number | null,
|
||||
minRemainingMs = accessRefreshSkewMs,
|
||||
@@ -193,10 +197,12 @@ export async function refreshStoredAuthSession(refreshToken = getStoredRefreshTo
|
||||
throw authRequiredError();
|
||||
}
|
||||
|
||||
const requestedRefreshToken = refreshToken;
|
||||
|
||||
if (!storedRefreshPromise) {
|
||||
storedRefreshPromise = publicClient
|
||||
.post<RefreshResponse, { refresh_token: string }>("/api/auth/refresh", {
|
||||
refresh_token: refreshToken,
|
||||
refresh_token: requestedRefreshToken,
|
||||
})
|
||||
.then((data) => {
|
||||
const tokens = toAuthTokens(data);
|
||||
@@ -204,7 +210,24 @@ export async function refreshStoredAuthSession(refreshToken = getStoredRefreshTo
|
||||
return tokens;
|
||||
})
|
||||
.catch((error) => {
|
||||
expireStoredSession();
|
||||
if (isAuthSessionExpiredError(error)) {
|
||||
const current = readStoredSession();
|
||||
if (
|
||||
current.accessToken &&
|
||||
current.refreshToken &&
|
||||
current.refreshToken !== requestedRefreshToken
|
||||
) {
|
||||
const tokens = {
|
||||
accessToken: current.accessToken,
|
||||
refreshToken: current.refreshToken,
|
||||
expiresAt: current.accessTokenExpiresAt ?? undefined,
|
||||
};
|
||||
setStoredTokens(tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
expireStoredSession();
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -798,7 +821,9 @@ async function subscribeSSE<TEvent extends SSEEventShape>(
|
||||
signal,
|
||||
});
|
||||
} catch (error) {
|
||||
expireStoredSession();
|
||||
if (isAuthSessionExpiredError(error)) {
|
||||
expireStoredSession();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ import { computed, ref } from "vue";
|
||||
|
||||
import type { LoginRequest, UserInfo } from "@geo/shared-types";
|
||||
|
||||
import { authApi, refreshStoredAuthSession, shouldRefreshAccessToken } from "@/lib/api";
|
||||
import {
|
||||
authApi,
|
||||
isAuthSessionExpiredError,
|
||||
refreshStoredAuthSession,
|
||||
shouldRefreshAccessToken,
|
||||
} from "@/lib/api";
|
||||
import {
|
||||
clearStoredSession,
|
||||
readStoredSession,
|
||||
@@ -61,11 +66,26 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
|
||||
try {
|
||||
await refreshStoredAuthSession(refreshToken.value);
|
||||
} catch {
|
||||
clearStoredSession({ notifyAuthExpired: true });
|
||||
} catch (error) {
|
||||
if (isAuthSessionExpiredError(error)) {
|
||||
clearStoredSession({ notifyAuthExpired: true });
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleRefreshRetry();
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRefreshRetry(): void {
|
||||
clearRefreshTimer();
|
||||
if (!refreshToken.value || typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
refreshTimer = window.setTimeout(() => {
|
||||
void refreshSession();
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
async function refreshSessionIfNeeded(minRemainingMs = 2 * 60 * 1000): Promise<void> {
|
||||
if (!refreshToken.value) {
|
||||
return;
|
||||
@@ -108,8 +128,10 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
const currentUser = await authApi.me();
|
||||
setStoredUser(currentUser);
|
||||
}
|
||||
} catch {
|
||||
clearStoredSession({ notifyAuthExpired: true });
|
||||
} catch (error) {
|
||||
if (isAuthSessionExpiredError(error)) {
|
||||
clearStoredSession({ notifyAuthExpired: true });
|
||||
}
|
||||
}
|
||||
|
||||
initialized.value = true;
|
||||
|
||||
Reference in New Issue
Block a user