Improve the dahboard 4

This commit is contained in:
Urtzi Alfaro
2025-08-18 20:50:41 +02:00
parent 523fc663e8
commit 18355cd8be
10 changed files with 1133 additions and 152 deletions

View File

@@ -13,21 +13,77 @@ import { ApiErrorHandler } from '../utils';
* Automatically adds authentication headers to requests
*/
class AuthInterceptor {
static isTokenExpired(token: string): boolean {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
return payload.exp <= currentTime;
} catch (error) {
console.warn('Error parsing token:', error);
return true; // Treat invalid tokens as expired
}
}
static isTokenExpiringSoon(token: string, bufferMinutes: number = 5): boolean {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
const bufferSeconds = bufferMinutes * 60;
return payload.exp <= (currentTime + bufferSeconds);
} catch (error) {
console.warn('Error parsing token for expiration check:', error);
return true;
}
}
static async refreshTokenIfNeeded(): Promise<void> {
const token = localStorage.getItem('auth_token');
const refreshToken = localStorage.getItem('refresh_token');
if (!token || !refreshToken) {
return;
}
// If token is expiring within 5 minutes, proactively refresh
if (this.isTokenExpiringSoon(token, 5)) {
try {
const baseURL = (apiClient as any).baseURL || window.location.origin;
const response = await fetch(`${baseURL}/api/v1/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('auth_token', data.access_token);
if (data.refresh_token) {
localStorage.setItem('refresh_token', data.refresh_token);
}
} else {
console.warn('Token refresh failed:', response.status);
}
} catch (error) {
console.warn('Token refresh error:', error);
}
}
}
static setup() {
apiClient.addRequestInterceptor({
onRequest: async (config: RequestConfig) => {
// Proactively refresh token if needed
await this.refreshTokenIfNeeded();
let token = localStorage.getItem('auth_token');
console.log('🔐 AuthInterceptor: Checking auth token...', token ? 'Found' : 'Missing');
if (token) {
console.log('🔐 AuthInterceptor: Token preview:', token.substring(0, 20) + '...');
}
// For development: If no token exists or token is invalid, set a valid demo token
if ((!token || token === 'demo-development-token') && window.location.hostname === 'localhost') {
console.log('🔧 AuthInterceptor: Development mode - setting valid demo token');
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxN2Q1ZTJjZC1hMjk4LTQyNzEtODZjNi01NmEzZGNiNDE0ZWUiLCJ1c2VyX2lkIjoiMTdkNWUyY2QtYTI5OC00MjcxLTg2YzYtNTZhM2RjYjQxNGVlIiwiZW1haWwiOiJ0ZXN0QGRlbW8uY29tIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NTI3MzEyNSwiaWF0IjoxNzU1MjcxMzI1LCJpc3MiOiJiYWtlcnktYXV0aCIsImZ1bGxfbmFtZSI6IkRlbW8gVXNlciIsImlzX3ZlcmlmaWVkIjpmYWxzZSwiaXNfYWN0aXZlIjp0cnVlLCJyb2xlIjoidXNlciJ9.RBfzH9L_NKySYkyLzBLYAApnrCFNK4OsGLLO-eCaTSI';
localStorage.setItem('auth_token', token);
// Check if token is expired
if (this.isTokenExpired(token)) {
console.warn('Token expired, removing from storage');
localStorage.removeItem('auth_token');
token = null;
}
}
if (token) {
@@ -35,9 +91,8 @@ class AuthInterceptor {
...config.headers,
Authorization: `Bearer ${token}`,
};
console.log('🔐 AuthInterceptor: Added Authorization header');
} else {
console.warn('⚠️ AuthInterceptor: No auth token found in localStorage');
console.warn('No valid auth token found - authentication required');
}
return config;
@@ -48,9 +103,6 @@ class AuthInterceptor {
throw error;
},
});
// Note: 401 handling is now managed by ErrorRecoveryInterceptor
// This allows token refresh to work before redirecting to login
}
}
@@ -179,8 +231,7 @@ class ErrorRecoveryInterceptor {
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
return apiClient.request(originalRequest.url, originalRequest);
return this.retryRequestWithNewToken(originalRequest, token);
}).catch(err => {
throw err;
});
@@ -196,7 +247,7 @@ class ErrorRecoveryInterceptor {
throw new Error('No refresh token available');
}
// Attempt to refresh token
// Use direct fetch to avoid interceptor recursion
const baseURL = (apiClient as any).baseURL || window.location.origin;
const response = await fetch(`${baseURL}/api/v1/auth/refresh`, {
method: 'POST',
@@ -207,22 +258,31 @@ class ErrorRecoveryInterceptor {
});
if (!response.ok) {
throw new Error('Token refresh failed');
throw new Error(`Token refresh failed: ${response.status}`);
}
const data = await response.json();
const newToken = data.access_token;
if (!newToken) {
throw new Error('No access token received');
}
localStorage.setItem('auth_token', newToken);
// Update new refresh token if provided
if (data.refresh_token) {
localStorage.setItem('refresh_token', data.refresh_token);
}
// Process failed queue
this.processQueue(null, newToken);
// Retry original request
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
return apiClient.request(originalRequest.url, originalRequest);
// Retry original request with new token
return this.retryRequestWithNewToken(originalRequest, newToken);
} catch (refreshError) {
console.warn('Token refresh failed:', refreshError);
this.processQueue(refreshError, null);
// Clear auth data and redirect to login
@@ -230,7 +290,8 @@ class ErrorRecoveryInterceptor {
localStorage.removeItem('refresh_token');
localStorage.removeItem('user_data');
if (typeof window !== 'undefined') {
// Only redirect if we're not already on the login page
if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
window.location.href = '/login';
}
@@ -245,6 +306,55 @@ class ErrorRecoveryInterceptor {
});
}
private static async retryRequestWithNewToken(originalRequest: any, token: string) {
try {
// Use direct fetch instead of apiClient to avoid interceptor recursion
const url = originalRequest.url || originalRequest.endpoint;
const method = originalRequest.method || 'GET';
const fetchOptions: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...originalRequest.headers
}
};
// Add body for non-GET requests
if (method !== 'GET' && originalRequest.body) {
fetchOptions.body = typeof originalRequest.body === 'string'
? originalRequest.body
: JSON.stringify(originalRequest.body);
}
// Add query parameters if present
let fullUrl = url;
if (originalRequest.params) {
const urlWithParams = new URL(fullUrl, (apiClient as any).baseURL);
Object.entries(originalRequest.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
urlWithParams.searchParams.append(key, String(value));
}
});
fullUrl = urlWithParams.toString();
}
// Retry request with refreshed token
const response = await fetch(fullUrl, fetchOptions);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
} catch (retryError) {
console.warn('Request retry failed:', retryError);
throw retryError;
}
}
private static processQueue(error: any, token: string | null) {
this.failedQueue.forEach(({ resolve, reject }) => {
if (error) {