fix: session expiry during usage and also login error message

This commit is contained in:
Nathnael
2026-07-07 08:52:35 +00:00
parent ff0fa6bb9e
commit 63f638d1bb
9 changed files with 340 additions and 58 deletions

View File

@@ -42,21 +42,41 @@ client.interceptors.request.use((config) => {
});
// Token refresh state
let isRefreshing = false;
let failedQueue: {
resolve: (token: string) => void;
reject: (error: unknown) => void;
}[] = [];
let refreshPromise: Promise<string> | null = null;
function processQueue(error: unknown, token?: string) {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else {
resolve(token!);
/**
* Single-flight token refresh: concurrent callers (the 401 interceptor and
* the proactive scheduler) share one in-flight request so the refresh token
* is only rotated once. Throws if no refresh token is stored or the server
* rejects it — callers decide how to end the session.
*/
async function refreshSessionTokens(): Promise<string> {
refreshPromise ??= (async () => {
const refreshToken = getCookie("refresh-token");
if (!refreshToken) {
throw new Error("missing refresh token");
}
type TokenPair = { token: string; refreshToken: string };
const { data } = await client.post<Partial<TokenPair> & { data?: TokenPair }>(
URL_CONSTANTS.AUTH.REFRESH_TOKEN,
{ refreshToken },
);
// The API returns the pair flat ({ success, token, refreshToken }); accept
// a { data: { ... } }-wrapped shape too so a transform change can't
// silently break refresh again.
const payload = data.data ?? data;
if (!payload.token || !payload.refreshToken) {
throw new Error("malformed refresh-token response");
}
setCookie("auth-token", payload.token, 7);
setCookie("refresh-token", payload.refreshToken, 7);
return payload.token;
})().finally(() => {
refreshPromise = null;
});
failedQueue = [];
return refreshPromise;
}
// Handle auth errors globally with token refresh
@@ -72,54 +92,41 @@ client.interceptors.response.use(
// - status is not 401
// - already retried
// - it's the refresh endpoint itself
// - it's a credential endpoint (401 there = wrong credentials, not an
// expired session — refreshing would mask the real error)
if (
!error.response ||
error.response.status !== 401 ||
originalRequest._retry ||
originalRequest.url === URL_CONSTANTS.AUTH.REFRESH_TOKEN
originalRequest.url === URL_CONSTANTS.AUTH.REFRESH_TOKEN ||
originalRequest.url === URL_CONSTANTS.AUTH.LOGIN ||
originalRequest.url === URL_CONSTANTS.USERS.SIGN_UP
) {
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return client(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = getCookie("refresh-token");
if (!refreshToken) {
isRefreshing = false;
clearAuthCookies();
// Nothing to refresh with (e.g. not logged in yet) — surface the
// original error instead of a confusing refresh failure.
if (!getCookie("refresh-token")) {
if (getCookie("auth-token")) {
// Half-broken cookie state; reset it.
clearAuthCookies();
}
return Promise.reject(error);
}
originalRequest._retry = true;
try {
const { data } = await client.post<{
data: { token: string; refreshToken: string };
}>(URL_CONSTANTS.AUTH.REFRESH_TOKEN, { refreshToken });
const { token, refreshToken: newRefreshToken } = data.data;
setCookie("auth-token", token, 7);
setCookie("refresh-token", newRefreshToken, 7);
const token = await refreshSessionTokens();
originalRequest.headers.Authorization = `Bearer ${token}`;
processQueue(null, token);
return client(originalRequest);
} catch (refreshError) {
processQueue(refreshError, undefined);
clearAuthCookies();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
export { client };
export { client, clearAuthCookies, getCookie, refreshSessionTokens };
export type { UseQueryOptions, QueryObserverOptions };