fix auth reload
This commit is contained in:
parent
0ac5b589e8
commit
a9bbbede4a
@ -21,8 +21,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', href: '/dashboard', icon: '📊' },
|
{ name: 'Dashboard', href: '/dashboard', icon: '📊' },
|
||||||
{ name: 'Bookings', href: '/dashboard/bookings', icon: '📦' },
|
{ name: 'Bookings', href: '/dashboard/bookings', icon: '📦' },
|
||||||
{ name: 'Search Rates', href: '/dashboard/search', icon: '🔍' },
|
{ name: 'Search Rates', href: '/dashboard/search-advanced', icon: '🔎' },
|
||||||
{ name: 'Search Advanced', href: '/dashboard/search-advanced', icon: '🔎' },
|
|
||||||
{ name: 'My Profile', href: '/dashboard/profile', icon: '👤' },
|
{ name: 'My Profile', href: '/dashboard/profile', icon: '👤' },
|
||||||
{ name: 'Organization', href: '/dashboard/settings/organization', icon: '🏢' },
|
{ name: 'Organization', href: '/dashboard/settings/organization', icon: '🏢' },
|
||||||
{ name: 'Users', href: '/dashboard/settings/users', icon: '👥' },
|
{ name: 'Users', href: '/dashboard/settings/users', icon: '👥' },
|
||||||
|
|||||||
@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
||||||
|
|
||||||
|
// Track if we're currently refreshing to avoid multiple simultaneous refresh requests
|
||||||
|
let isRefreshing = false;
|
||||||
|
let refreshSubscribers: Array<(token: string) => void> = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get authentication token from localStorage
|
* Get authentication token from localStorage
|
||||||
*/
|
*/
|
||||||
@ -38,6 +42,61 @@ export function clearAuthTokens(): void {
|
|||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
localStorage.removeItem('refresh_token');
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add subscriber to be notified when token is refreshed
|
||||||
|
*/
|
||||||
|
function subscribeTokenRefresh(callback: (token: string) => void): void {
|
||||||
|
refreshSubscribers.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify all subscribers that token has been refreshed
|
||||||
|
*/
|
||||||
|
function onTokenRefreshed(token: string): void {
|
||||||
|
refreshSubscribers.forEach(callback => callback(token));
|
||||||
|
refreshSubscribers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh access token using refresh token
|
||||||
|
*/
|
||||||
|
async function refreshAccessToken(): Promise<string> {
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error('No refresh token available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/v1/auth/refresh`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ refreshToken }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// Refresh token invalid or expired, clear everything
|
||||||
|
clearAuthTokens();
|
||||||
|
// Redirect to login
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
throw new Error('Failed to refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const newAccessToken = data.accessToken;
|
||||||
|
|
||||||
|
// Update access token in localStorage (keep same refresh token)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('access_token', newAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAccessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,9 +148,13 @@ export class ApiError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make API request
|
* Make API request with automatic token refresh on 401
|
||||||
*/
|
*/
|
||||||
export async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
export async function apiRequest<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {},
|
||||||
|
isRetry = false
|
||||||
|
): Promise<T> {
|
||||||
const url = `${API_BASE_URL}${endpoint}`;
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@ -101,6 +164,73 @@ export async function apiRequest<T>(endpoint: string, options: RequestInit = {})
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized - token expired
|
||||||
|
if (response.status === 401 && !isRetry && !endpoint.includes('/auth/refresh')) {
|
||||||
|
// Check if we have a refresh token
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
// No refresh token, redirect to login
|
||||||
|
clearAuthTokens();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
throw new ApiError('Session expired', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to refresh the token
|
||||||
|
try {
|
||||||
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
const newAccessToken = await refreshAccessToken();
|
||||||
|
isRefreshing = false;
|
||||||
|
onTokenRefreshed(newAccessToken);
|
||||||
|
|
||||||
|
// Retry the original request with new token
|
||||||
|
const newHeaders = { ...options.headers };
|
||||||
|
if (newHeaders && typeof newHeaders === 'object' && 'Authorization' in newHeaders) {
|
||||||
|
(newHeaders as any)['Authorization'] = `Bearer ${newAccessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRequest<T>(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
headers: newHeaders,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Already refreshing, wait for the new token
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
subscribeTokenRefresh(async (newAccessToken: string) => {
|
||||||
|
const newHeaders = { ...options.headers };
|
||||||
|
if (newHeaders && typeof newHeaders === 'object' && 'Authorization' in newHeaders) {
|
||||||
|
(newHeaders as any)['Authorization'] = `Bearer ${newAccessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiRequest<T>(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
headers: newHeaders,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (refreshError) {
|
||||||
|
isRefreshing = false;
|
||||||
|
refreshSubscribers = [];
|
||||||
|
throw refreshError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json().catch(() => ({}));
|
const error = await response.json().catch(() => ({}));
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
@ -176,6 +306,42 @@ export async function upload<T>(
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized for file uploads
|
||||||
|
if (response.status === 401) {
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
if (refreshToken) {
|
||||||
|
try {
|
||||||
|
const newAccessToken = await refreshAccessToken();
|
||||||
|
// Retry upload with new token
|
||||||
|
const retryResponse = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...createMultipartHeaders(includeAuth),
|
||||||
|
Authorization: `Bearer ${newAccessToken}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!retryResponse.ok) {
|
||||||
|
const error = await retryResponse.json().catch(() => ({}));
|
||||||
|
throw new ApiError(
|
||||||
|
error.message || `Upload failed: ${retryResponse.statusText}`,
|
||||||
|
retryResponse.status,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return retryResponse.json();
|
||||||
|
} catch (refreshError) {
|
||||||
|
clearAuthTokens();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
throw refreshError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json().catch(() => ({}));
|
const error = await response.json().catch(() => ({}));
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
@ -203,6 +369,45 @@ export async function download(
|
|||||||
headers: createHeaders(includeAuth),
|
headers: createHeaders(includeAuth),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized for downloads
|
||||||
|
if (response.status === 401) {
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
if (refreshToken) {
|
||||||
|
try {
|
||||||
|
const newAccessToken = await refreshAccessToken();
|
||||||
|
// Retry download with new token
|
||||||
|
const retryResponse = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...createHeaders(includeAuth),
|
||||||
|
Authorization: `Bearer ${newAccessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!retryResponse.ok) {
|
||||||
|
throw new ApiError(`Download failed: ${retryResponse.statusText}`, retryResponse.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await retryResponse.blob();
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(downloadUrl);
|
||||||
|
return;
|
||||||
|
} catch (refreshError) {
|
||||||
|
clearAuthTokens();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
throw refreshError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new ApiError(`Download failed: ${response.statusText}`, response.status);
|
throw new ApiError(`Download failed: ${response.statusText}`, response.status);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,23 +57,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
if (isAuthenticated()) {
|
if (isAuthenticated()) {
|
||||||
// Try to fetch current user from API
|
// Try to fetch current user from API (will auto-refresh token if expired)
|
||||||
try {
|
try {
|
||||||
const currentUser = await getCurrentUser();
|
const currentUser = await getCurrentUser();
|
||||||
setUser(currentUser);
|
setUser(currentUser);
|
||||||
// Update stored user
|
// Update stored user
|
||||||
localStorage.setItem('user', JSON.stringify(currentUser));
|
localStorage.setItem('user', JSON.stringify(currentUser));
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
console.error('Failed to fetch user from API, checking localStorage:', apiError);
|
console.error('Failed to fetch user from API:', apiError);
|
||||||
// If API fails, try to use stored user as fallback
|
// If API fails after token refresh attempt, clear everything
|
||||||
const storedUser = getStoredUser();
|
if (typeof window !== 'undefined') {
|
||||||
if (storedUser) {
|
localStorage.removeItem('access_token');
|
||||||
console.log('Using stored user as fallback:', storedUser);
|
localStorage.removeItem('refresh_token');
|
||||||
setUser(storedUser);
|
localStorage.removeItem('user');
|
||||||
} else {
|
|
||||||
// No stored user and API failed - clear everything
|
|
||||||
throw apiError;
|
|
||||||
}
|
}
|
||||||
|
setUser(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -91,6 +89,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
checkAuth();
|
checkAuth();
|
||||||
|
|
||||||
|
// Check token validity every 5 minutes and refresh if needed
|
||||||
|
const tokenCheckInterval = setInterval(async () => {
|
||||||
|
if (isAuthenticated()) {
|
||||||
|
try {
|
||||||
|
// This will automatically refresh the token if it's expired (via API client)
|
||||||
|
await getCurrentUser();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token validation failed:', error);
|
||||||
|
// If token refresh fails, user will be redirected to login by the API client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5 * 60 * 1000); // 5 minutes
|
||||||
|
|
||||||
|
return () => clearInterval(tokenCheckInterval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user