xpeditis2.0/apps/frontend/lib/api/client.ts
David d65cb721b5
Some checks are pending
CD Production (Hetzner k3s) / Promote Images (preprod → prod) (push) Waiting to run
CD Production (Hetzner k3s) / Deploy to k3s (xpeditis-prod) (push) Blocked by required conditions
CD Production (Hetzner k3s) / Smoke Tests (push) Blocked by required conditions
CD Production (Hetzner k3s) / Deployment Summary (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Success (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Failure (push) Blocked by required conditions
chore: sync full codebase from cicd branch
Aligns main with the complete application codebase (cicd branch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:56:44 +02:00

127 lines
3.6 KiB
TypeScript

/**
* API Client
*
* Axios-based API client with authentication support
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
export class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
this.client.interceptors.request.use(
config => {
const token = this.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// Response interceptor to handle token refresh
this.client.interceptors.response.use(
response => response,
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & {
_retry?: boolean;
};
// If 401 and not already retried, try to refresh token
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = this.getRefreshToken();
if (refreshToken) {
const { data } = await axios.post(`${API_BASE_URL}/api/v1/auth/refresh`, {
refreshToken,
});
this.setAccessToken(data.accessToken);
// Retry original request with new token
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
}
return this.client(originalRequest);
}
} catch (refreshError) {
// Refresh failed, clear tokens and redirect to login
this.clearTokens();
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
}
private getAccessToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('accessToken');
}
private getRefreshToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('refreshToken');
}
private setAccessToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('accessToken', token);
}
}
private clearTokens(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<T>(url, config);
return response.data;
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<T>(url, data, config);
return response.data;
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.put<T>(url, data, config);
return response.data;
}
async patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.patch<T>(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, config);
return response.data;
}
}
export const apiClient = new ApiClient();