feat(auth): implement refresh token flow with 401 auto-retry and dedup
This commit is contained in:
parent
8ad31af72a
commit
5e8cbbf556
@ -10,6 +10,7 @@ import {
|
||||
registerUser,
|
||||
getMe,
|
||||
setAuthToken,
|
||||
setRefreshToken,
|
||||
setSyncEnabled,
|
||||
isAuthenticated as checkAuth,
|
||||
type AuthUser,
|
||||
@ -57,6 +58,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
const result = await loginUser(email, password);
|
||||
setAuthToken(result.accessToken);
|
||||
setRefreshToken(result.refreshToken);
|
||||
setSyncEnabled(true);
|
||||
setUser(result.user);
|
||||
return true;
|
||||
@ -72,6 +74,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
const result = await registerUser(email, password, displayName);
|
||||
setAuthToken(result.accessToken);
|
||||
setRefreshToken(result.refreshToken);
|
||||
setSyncEnabled(true);
|
||||
setUser(result.user);
|
||||
return true;
|
||||
|
||||
@ -57,6 +57,7 @@ export interface OfflineQueueItem {
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
authToken: 'chronomind-auth-token',
|
||||
refreshToken: 'chronomind-refresh-token',
|
||||
lastSync: 'chronomind-platform-last-sync',
|
||||
offlineQueue: 'chronomind-offline-queue',
|
||||
syncEnabled: 'chronomind-platform-sync-enabled',
|
||||
@ -76,6 +77,68 @@ function getAuthToken(): string | null {
|
||||
return localStorage.getItem(STORAGE_KEYS.authToken);
|
||||
}
|
||||
|
||||
function getRefreshToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(STORAGE_KEYS.refreshToken);
|
||||
}
|
||||
|
||||
export function setRefreshToken(token: string | null): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (token) {
|
||||
localStorage.setItem(STORAGE_KEYS.refreshToken, token);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEYS.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
let _refreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
/**
|
||||
* Attempt to refresh the access token using the stored refresh token.
|
||||
* Returns true if refresh succeeded, false otherwise.
|
||||
* Deduplicates concurrent refresh attempts.
|
||||
*/
|
||||
export async function refreshAccessToken(): Promise<boolean> {
|
||||
if (_refreshPromise) return _refreshPromise;
|
||||
|
||||
_refreshPromise = (async () => {
|
||||
const rt = getRefreshToken();
|
||||
if (!rt) return false;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${getBaseUrl()}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-product-id': PRODUCT_ID,
|
||||
'x-request-id': crypto.randomUUID(),
|
||||
},
|
||||
body: JSON.stringify({ refreshToken: rt }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
// Refresh token is invalid/expired — clear both tokens
|
||||
setAuthToken(null);
|
||||
setRefreshToken(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await res.json() as { accessToken: string; refreshToken: string };
|
||||
setAuthToken(data.accessToken);
|
||||
setRefreshToken(data.refreshToken);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
return await _refreshPromise;
|
||||
} finally {
|
||||
_refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiRequest<T>(
|
||||
path: string,
|
||||
method: string,
|
||||
@ -100,6 +163,14 @@ async function apiRequest<T>(
|
||||
throw new SyncConflictError(data);
|
||||
}
|
||||
|
||||
// On 401, attempt a silent token refresh and retry once
|
||||
if (res.status === 401 && !path.startsWith('/auth/')) {
|
||||
const refreshed = await refreshAccessToken();
|
||||
if (refreshed) {
|
||||
return apiRequest<T>(path, method, body);
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Sync API error: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
@ -122,6 +193,7 @@ export function setAuthToken(token: string | null): void {
|
||||
localStorage.setItem(STORAGE_KEYS.authToken, token);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEYS.authToken);
|
||||
localStorage.removeItem(STORAGE_KEYS.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user