import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; import { API_URL } from '../utils/config'; import { getToken, setToken, removeToken, removeUser, getRefreshToken, removeRefreshToken } from '../utils/storage'; let authInitialized = false; let isRedirectingToLogin = false; let isRefreshing = false; let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: any) => void; }> = []; function processQueue(error: any, token: string | null = null) { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token!); } }); failedQueue = []; } export function setAuthInitialized(value: boolean): void { authInitialized = value; if (value === true) { isRedirectingToLogin = false; } } export function resetRedirectFlag(): void { isRedirectingToLogin = false; } export interface ApiError { message: string; status?: number; code?: string; } class ApiService { private axiosInstance: AxiosInstance; constructor() { this.axiosInstance = axios.create({ baseURL: API_URL, timeout: 30000, // 30 seconds timeout headers: { 'Content-Type': 'application/json', }, }); // Request interceptor: Add Authorization header with JWT this.axiosInstance.interceptors.request.use( (config) => { const token = getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { console.error('Request interceptor error:', error); return Promise.reject(this.handleError(error)); } ); // Response interceptor: Handle errors this.axiosInstance.interceptors.response.use( (response) => response, async (error: AxiosError) => { if (error.response?.status === 401) { const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }; // Don't attempt refresh during auth initialization or if already retried if (!authInitialized || originalRequest._retry) { if (authInitialized && !isRedirectingToLogin) { isRedirectingToLogin = true; removeToken(); removeRefreshToken(); removeUser(); window.location.href = '/login'; } return Promise.reject(this.handleError(error)); } if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve: (token: string) => { originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${token}` }; resolve(this.axiosInstance.request(originalRequest)); }, reject: (err: any) => { reject(err); }, }); }); } originalRequest._retry = true; isRefreshing = true; const refreshToken = getRefreshToken(); if (!refreshToken) { isRefreshing = false; if (!isRedirectingToLogin) { isRedirectingToLogin = true; removeToken(); removeRefreshToken(); removeUser(); window.location.href = '/login'; } return Promise.reject(this.handleError(error)); } try { // Use a raw axios call (not the intercepted instance) to avoid loops const response = await axios.post(`${API_URL}/api/auth/refresh`, { refreshToken }); const newToken = response.data.data.accessToken; setToken(newToken); processQueue(null, newToken); originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${newToken}` }; return this.axiosInstance.request(originalRequest); } catch (refreshError) { processQueue(refreshError, null); if (!isRedirectingToLogin) { isRedirectingToLogin = true; removeToken(); removeRefreshToken(); removeUser(); window.location.href = '/login'; } return Promise.reject(this.handleError(error)); } finally { isRefreshing = false; } } // Retry on 429 (Too Many Requests) with exponential backoff if (error.response?.status === 429 && error.config) { const config = error.config as AxiosRequestConfig & { _retryCount?: number }; const retryCount = config._retryCount || 0; const maxRetries = 3; if (retryCount < maxRetries) { config._retryCount = retryCount + 1; const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s console.warn(`Rate limited (429). Retrying in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`); await new Promise((resolve) => setTimeout(resolve, delay)); return this.axiosInstance.request(config); } } return Promise.reject(this.handleError(error)); } ); } private handleError(error: AxiosError): ApiError { if (error.response) { // Server responded with error const message = (error.response.data as any)?.message || (error.response.data as any)?.error || error.message || 'Ein Fehler ist aufgetreten'; return { message, status: error.response.status, code: error.code, }; } else if (error.request) { // Request was made but no response received return { message: 'Keine Antwort vom Server. Bitte überprüfen Sie Ihre Internetverbindung.', code: error.code, }; } else { // Something else happened return { message: error.message || 'Ein unerwarteter Fehler ist aufgetreten', code: error.code, }; } } async get(url: string, config?: AxiosRequestConfig): Promise> { return this.axiosInstance.get(url, config); } async post(url: string, data?: any, config?: AxiosRequestConfig): Promise> { return this.axiosInstance.post(url, data, config); } async put(url: string, data?: any, config?: AxiosRequestConfig): Promise> { return this.axiosInstance.put(url, data, config); } async delete(url: string, config?: AxiosRequestConfig): Promise> { return this.axiosInstance.delete(url, config); } async patch(url: string, data?: any, config?: AxiosRequestConfig): Promise> { return this.axiosInstance.patch(url, data, config); } } export const api = new ApiService();