Add JWT auto-refresh: store refresh_token at login, retry on 401
This commit is contained in:
parent
91971379c5
commit
f071e72678
2 changed files with 77 additions and 12 deletions
|
|
@ -120,15 +120,76 @@ export const CONFIG = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
|
||||||
const token = localStorage.getItem('access_token');
|
let isRefreshing = false;
|
||||||
const headers = {
|
let refreshSubscribers: ((token: string) => void)[] = [];
|
||||||
...options.headers,
|
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
function onTokenRefreshed(token: string) {
|
||||||
};
|
refreshSubscribers.forEach(cb => cb(token));
|
||||||
return fetch(url, {
|
refreshSubscribers = [];
|
||||||
...options,
|
|
||||||
headers,
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshAccessToken(): Promise<string | null> {
|
||||||
|
const refreshToken = localStorage.getItem('refresh_token');
|
||||||
|
if (!refreshToken) return null;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${CONFIG.apiBaseUrl}/api/auth/token/refresh/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refresh: refreshToken }),
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json();
|
||||||
|
const newToken = data.access;
|
||||||
|
if (newToken) {
|
||||||
|
localStorage.setItem('access_token', newToken);
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const makeRequest = (authToken: string | null) =>
|
||||||
|
fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {}),
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await makeRequest(token);
|
||||||
|
|
||||||
|
// Auto-refresh on 401
|
||||||
|
if (response.status === 401) {
|
||||||
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
const newToken = await refreshAccessToken();
|
||||||
|
isRefreshing = false;
|
||||||
|
if (newToken) {
|
||||||
|
onTokenRefreshed(newToken);
|
||||||
|
return makeRequest(newToken);
|
||||||
|
} else {
|
||||||
|
// Refresh failed — clear tokens
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
refreshSubscribers = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Queue the request until refresh completes
|
||||||
|
return new Promise(resolve => {
|
||||||
|
refreshSubscribers.push((newToken: string) => {
|
||||||
|
resolve(makeRequest(newToken));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,8 @@ export const SellerProvider: React.FC<{children: React.ReactNode}> = ({ children
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Session restore failed:', err);
|
console.error('Session restore failed:', err);
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -389,6 +390,7 @@ export const SellerProvider: React.FC<{children: React.ReactNode}> = ({ children
|
||||||
.catch(err => console.error('Error logging out:', err))
|
.catch(err => console.error('Error logging out:', err))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
localStorage.removeItem('access_token')
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
// Reset all onboarding & profile states to their defaults
|
// Reset all onboarding & profile states to their defaults
|
||||||
setIsProfileComplete(false)
|
setIsProfileComplete(false)
|
||||||
setProfileStep(1)
|
setProfileStep(1)
|
||||||
|
|
@ -491,6 +493,7 @@ export const SellerProvider: React.FC<{children: React.ReactNode}> = ({ children
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
localStorage.setItem('access_token', data.access_token);
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
if (data.refresh_token) localStorage.setItem('refresh_token', data.refresh_token);
|
||||||
navigateTo('profile-completion')
|
navigateTo('profile-completion')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -518,6 +521,7 @@ export const SellerProvider: React.FC<{children: React.ReactNode}> = ({ children
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
localStorage.setItem('access_token', data.access_token);
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
if (data.refresh_token) localStorage.setItem('refresh_token', data.refresh_token);
|
||||||
if (data.user && data.user.profile) {
|
if (data.user && data.user.profile) {
|
||||||
const profile = data.user.profile;
|
const profile = data.user.profile;
|
||||||
setPhone(profile.phone || '');
|
setPhone(profile.phone || '');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue