210 lines
6.8 KiB
TypeScript
210 lines
6.8 KiB
TypeScript
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<AxiosResponse>((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<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.get<T>(url, config);
|
|
}
|
|
|
|
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.post<T>(url, data, config);
|
|
}
|
|
|
|
async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.put<T>(url, data, config);
|
|
}
|
|
|
|
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.delete<T>(url, config);
|
|
}
|
|
|
|
async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.patch<T>(url, data, config);
|
|
}
|
|
}
|
|
|
|
export const api = new ApiService();
|