Files
plagiat/src/shared/request/apiRequest.ts
nabijonovdavronbek619@gmail.com aea8854a13 add service and sertificate prices ,
2026-04-18 13:50:32 +05:00

251 lines
8.9 KiB
TypeScript

import axios, {
AxiosRequestConfig,
AxiosResponse,
AxiosError,
InternalAxiosRequestConfig,
} from 'axios';
import { toast } from 'react-toastify';
import { getRouteLang } from './getLanguage';
// ─── Error message extractor ───────────────────────────────────────────────────
function extractErrorMessage(error: AxiosError): string {
const data = error.response?.data as Record<string, unknown> | undefined;
if (!data) {
if (error.code === 'ECONNABORTED')
return 'Request timed out. Please try again.';
if (!navigator.onLine) return 'No internet connection.';
return error.message || 'An unexpected error occurred.';
}
// Simple string fields: { message, detail, error }
if (typeof data.message === 'string' && data.message) return data.message;
if (typeof data.detail === 'string' && data.detail) return data.detail;
if (typeof data.error === 'string' && data.error) return data.error;
// Wrapped: { errors: { field: ["msg"] } }
if (data.errors && typeof data.errors === 'object') {
const first = Object.values(data.errors as Record<string, unknown>)[0];
if (Array.isArray(first) && first.length > 0) return String(first[0]);
if (typeof first === 'string') return first;
}
// DRF field-level errors at top level: { phone: ["msg"], name: ["msg"] }
for (const val of Object.values(data)) {
if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') {
return val[0];
}
if (typeof val === 'string' && val) return val;
}
return 'An unexpected error occurred.';
}
// ─── Constants ─────────────────────────────────────────────────────────────────
// const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
const baseUrl = 'https://api.anti-plagiat.uz/api/v1';
const DEFAULT_LOCALE = 'uz'; // fallback locale for redirect
// ─── Token helpers ─────────────────────────────────────────────────────────────
// Adjust key names to match whatever your auth stores them under.
export const TokenStorage = {
getAccess: (): string | null => localStorage.getItem('access_token'),
getRefresh: (): string | null => localStorage.getItem('refresh_token'),
setAccess: (token: string): void =>
localStorage.setItem('access_token', token),
setRefresh: (token: string): void =>
localStorage.setItem('refresh_token', token),
setTokens: (access: string, refresh: string): void => {
localStorage.setItem('access_token', access);
localStorage.setItem('refresh_token', refresh);
},
clear: (): void => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
},
};
// ─── Redirect helper ───────────────────────────────────────────────────────────
function redirectToMain(): void {
// Detect current locale from the URL path, fall back to DEFAULT_LOCALE
const pathLocale = window.location.pathname.split('/')[1];
const validLocales = ['uz', 'ru', 'en'];
const locale = validLocales.includes(pathLocale)
? pathLocale
: DEFAULT_LOCALE;
window.location.href = `/${locale}`;
}
// ─── Axios instance ────────────────────────────────────────────────────────────
const api = axios.create({
baseURL: baseUrl,
timeout: 15_000,
});
// ─── Flag to prevent multiple simultaneous refresh calls ──────────────────────
let isRefreshing = false;
// Queue of failed requests waiting for the new token
type FailedRequestResolver = (token: string) => void;
let failedQueue: {
resolve: FailedRequestResolver;
reject: (err: unknown) => void;
}[] = [];
function processQueue(error: unknown, token: string | null = null): void {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else if (token) {
resolve(token);
}
});
failedQueue = [];
}
// ─── Request interceptor — attach access token ────────────────────────────────
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = TokenStorage.getAccess();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
config.headers['Accept-Language'] = getRouteLang();
return config;
},
(error: AxiosError) => Promise.reject(error),
);
// ─── Response interceptor — handle 401, refresh, retry ───────────────────────
api.interceptors.response.use(
// ── 2xx: pass through unchanged ────────────────────────────────────────────
(response: AxiosResponse) => response,
// ── Error: attempt token refresh on 401 ────────────────────────────────────
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
const status = error.response?.status;
// const responseData = error.response?.data as Record<string, unknown> | undefined;
const requestUrl = originalRequest.url ?? '';
const isAuthEndpoint =
requestUrl.includes('/users/login/') ||
requestUrl.includes('/users/register/');
// 403 with token_not_valid means the token is expired — clear and redirect
if (status === 403) {
TokenStorage.clear();
redirectToMain();
return Promise.reject(error);
}
// For auth endpoints, 401 means wrong credentials — show error, don't refresh
if (isAuthEndpoint || status !== 401 || originalRequest._retry) {
toast.error(extractErrorMessage(error));
return Promise.reject(error);
}
const refreshToken = TokenStorage.getRefresh();
// No refresh token available — clear everything and redirect
if (!refreshToken) {
TokenStorage.clear();
redirectToMain();
return Promise.reject(error);
}
// If a refresh is already in progress, queue this request
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(api(originalRequest));
},
reject,
});
});
}
// Mark that we're refreshing and this request has been retried
originalRequest._retry = true;
isRefreshing = true;
try {
// ── Call your refresh endpoint ──────────────────────────────────────────
// Adjust the URL and payload shape to match your backend.
const { data } = await axios.post<{
access_token: string;
refresh_token: string;
}>(
`${baseUrl}/auth/refresh`, // ← your refresh endpoint
{ refresh_token: refreshToken },
{ headers: { 'Accept-Language': getRouteLang() } },
);
const { access_token, refresh_token } = data;
TokenStorage.setTokens(access_token, refresh_token);
// Update the Authorization header for the retried request
originalRequest.headers.Authorization = `Bearer ${access_token}`;
// Unblock all queued requests with the new token
processQueue(null, access_token);
// Retry the original failed request
return api(originalRequest);
} catch (refreshError) {
// Refresh itself failed — clear tokens and redirect to home
processQueue(refreshError, null);
TokenStorage.clear();
redirectToMain();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
// ─── Public request function ───────────────────────────────────────────────────
export const apiRequest = async <T>(
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
url: string,
data?: unknown,
config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>,
): Promise<AxiosResponse<T>> => {
const response = await api.request<T>({
method,
url,
data,
...config,
headers: {
...config?.headers,
// Accept-Language is already set in the request interceptor,
// but config?.headers can still override it if needed.
},
});
console.log('resposne: ', response);
return response;
};