134 lines
4 KiB
TypeScript
134 lines
4 KiB
TypeScript
import { authService } from '~/services/auth.service';
|
|
|
|
let isRefreshing = false;
|
|
let refreshPromise: Promise<string> | null = null;
|
|
|
|
/**
|
|
* Get token or refresh if needed
|
|
* Handles concurrent requests by sharing same refresh promise
|
|
*/
|
|
export const getValidToken = async (): Promise<string | null> => {
|
|
const tokenCookie = useCookie('token');
|
|
const refreshTokenCookie = useCookie('refreshToken');
|
|
|
|
// If we have token, return it
|
|
if (tokenCookie.value) {
|
|
return tokenCookie.value;
|
|
}
|
|
|
|
// No token but have refresh token, try to refresh
|
|
if (refreshTokenCookie.value) {
|
|
// If already refreshing, wait for that promise
|
|
if (isRefreshing && refreshPromise) {
|
|
return refreshPromise;
|
|
}
|
|
|
|
try {
|
|
isRefreshing = true;
|
|
const refreshToken = refreshTokenCookie.value;
|
|
refreshPromise = authService.refreshToken(refreshToken).then(res => {
|
|
// Update cookies
|
|
useCookie('token').value = res.token;
|
|
useCookie('refreshToken').value = res.refreshToken;
|
|
return res.token;
|
|
});
|
|
const newToken = await refreshPromise;
|
|
return newToken;
|
|
} catch (error) {
|
|
console.error('Token refresh failed');
|
|
return null;
|
|
} finally {
|
|
isRefreshing = false;
|
|
refreshPromise = null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Handle 401 error - try to refresh and return new token
|
|
* Returns null if refresh fails
|
|
*/
|
|
export const handleUnauthorized = async (): Promise<string | null> => {
|
|
const refreshTokenCookie = useCookie('refreshToken');
|
|
|
|
if (!refreshTokenCookie.value) {
|
|
return null;
|
|
}
|
|
|
|
// If already refreshing, wait for that promise
|
|
if (isRefreshing && refreshPromise) {
|
|
return refreshPromise;
|
|
}
|
|
|
|
try {
|
|
isRefreshing = true;
|
|
const refreshToken = refreshTokenCookie.value;
|
|
refreshPromise = authService.refreshToken(refreshToken).then(res => {
|
|
// Update cookies
|
|
useCookie('token').value = res.token;
|
|
useCookie('refreshToken').value = res.refreshToken;
|
|
return res.token;
|
|
});
|
|
const newToken = await refreshPromise;
|
|
console.log('Token refreshed successfully');
|
|
return newToken;
|
|
} catch (error) {
|
|
console.error('Token refresh failed, need to login again');
|
|
// Clear all auth cookies
|
|
useCookie('token').value = null;
|
|
useCookie('refreshToken').value = null;
|
|
useCookie('user').value = null;
|
|
return null;
|
|
} finally {
|
|
isRefreshing = false;
|
|
refreshPromise = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create authenticated fetch with auto token refresh
|
|
*/
|
|
export const createAuthFetch = () => {
|
|
const config = useRuntimeConfig();
|
|
|
|
return async <T>(
|
|
url: string,
|
|
options: {
|
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
body?: any;
|
|
headers?: Record<string, string>;
|
|
} = {}
|
|
): Promise<T> => {
|
|
const token = await getValidToken();
|
|
|
|
const makeRequest = async (authToken: string | null) => {
|
|
return await $fetch<T>(url, {
|
|
baseURL: config.public.apiBaseUrl as string,
|
|
method: options.method || 'GET',
|
|
headers: {
|
|
...options.headers,
|
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {})
|
|
},
|
|
body: options.body
|
|
});
|
|
};
|
|
|
|
try {
|
|
return await makeRequest(token);
|
|
} catch (error: any) {
|
|
// If 401, try to refresh
|
|
if (error.response?.status === 401) {
|
|
const newToken = await handleUnauthorized();
|
|
if (newToken) {
|
|
// Retry with new token
|
|
return await makeRequest(newToken);
|
|
}
|
|
// Redirect to login
|
|
navigateTo('/login');
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
};
|