Improve the inventory page

This commit is contained in:
Urtzi Alfaro
2025-09-17 16:06:30 +02:00
parent 7aa26d51d3
commit dcb3ce441b
39 changed files with 5852 additions and 1762 deletions

View File

@@ -23,6 +23,13 @@ class ApiClient {
private baseURL: string;
private authToken: string | null = null;
private tenantId: string | null = null;
private refreshToken: string | null = null;
private isRefreshing: boolean = false;
private failedQueue: Array<{
resolve: (value?: any) => void;
reject: (error?: any) => void;
config: AxiosRequestConfig;
}> = [];
constructor(baseURL: string = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api/v1') {
this.baseURL = baseURL;
@@ -57,10 +64,58 @@ class ApiClient {
}
);
// Response interceptor for error handling
// Response interceptor for error handling and automatic token refresh
this.client.interceptors.response.use(
(response) => response,
(error) => {
async (error) => {
const originalRequest = error.config;
// Check if error is 401 and we have a refresh token
if (error.response?.status === 401 && this.refreshToken && !originalRequest._retry) {
if (this.isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject, config: originalRequest });
});
}
originalRequest._retry = true;
this.isRefreshing = true;
try {
// Attempt to refresh the token
const response = await this.client.post('/auth/refresh', {
refresh_token: this.refreshToken
});
const { access_token, refresh_token } = response.data;
// Update tokens
this.setAuthToken(access_token);
if (refresh_token) {
this.setRefreshToken(refresh_token);
}
// Update auth store if available
await this.updateAuthStore(access_token, refresh_token);
// Process failed queue
this.processQueue(null, access_token);
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${access_token}`;
return this.client(originalRequest);
} catch (refreshError) {
// Refresh failed, clear tokens and redirect to login
this.processQueue(refreshError, null);
await this.handleAuthFailure();
return Promise.reject(this.handleError(refreshError as AxiosError));
} finally {
this.isRefreshing = false;
}
}
return Promise.reject(this.handleError(error));
}
);
@@ -90,11 +145,69 @@ class ApiClient {
}
}
private processQueue(error: any, token: string | null = null) {
this.failedQueue.forEach(({ resolve, reject, config }) => {
if (error) {
reject(error);
} else {
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
resolve(this.client(config));
}
});
this.failedQueue = [];
}
private async updateAuthStore(accessToken: string, refreshToken?: string) {
try {
// Dynamically import to avoid circular dependency
const { useAuthStore } = await import('../../stores/auth.store');
const store = useAuthStore.getState();
// Update the store with new tokens
store.token = accessToken;
if (refreshToken) {
store.refreshToken = refreshToken;
}
} catch (error) {
console.warn('Failed to update auth store:', error);
}
}
private async handleAuthFailure() {
try {
// Clear tokens
this.setAuthToken(null);
this.setRefreshToken(null);
// Dynamically import to avoid circular dependency
const { useAuthStore } = await import('../../stores/auth.store');
const store = useAuthStore.getState();
// Logout user
store.logout();
// Redirect to login if not already there
if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
window.location.href = '/login';
}
} catch (error) {
console.warn('Failed to handle auth failure:', error);
}
}
// Configuration methods
setAuthToken(token: string | null) {
this.authToken = token;
}
setRefreshToken(token: string | null) {
this.refreshToken = token;
}
setTenantId(tenantId: string | null) {
this.tenantId = tenantId;
}
@@ -103,6 +216,10 @@ class ApiClient {
return this.authToken;
}
getRefreshToken(): string | null {
return this.refreshToken;
}
getTenantId(): string | null {
return this.tenantId;
}