251 lines
8.9 KiB
TypeScript
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;
|
|
};
|