2026-05-08 19:59:43 +03:00
|
|
|
import axios, { type AxiosInstance } from 'axios';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Axios-инстанс для API-запросов.
|
|
|
|
|
*
|
|
|
|
|
* Sanctum SPA mode (см. `bootstrap/app.php` + `routes/web.php` § /api/auth/*):
|
|
|
|
|
* 1. `withCredentials: true` — отправляем session-cookie + XSRF-TOKEN cookie.
|
|
|
|
|
* 2. `withXSRFToken: true` — axios читает XSRF-TOKEN cookie и кладёт
|
|
|
|
|
* в `X-XSRF-TOKEN` header автоматически (Laravel валидирует CSRF).
|
|
|
|
|
* 3. На первый запрос — `await ensureCsrfCookie()` забирает CSRF-cookie через
|
|
|
|
|
* `GET /sanctum/csrf-cookie` (выставляется один раз за сессию).
|
|
|
|
|
*
|
|
|
|
|
* baseURL не указываем — используем относительные пути (`/api/auth/login`),
|
|
|
|
|
* браузер шлёт same-origin → cookie работают без CORS-настроек.
|
|
|
|
|
*/
|
|
|
|
|
export const apiClient: AxiosInstance = axios.create({
|
|
|
|
|
withCredentials: true,
|
|
|
|
|
withXSRFToken: true,
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let csrfCookieFetched = false;
|
|
|
|
|
|
|
|
|
|
export async function ensureCsrfCookie(): Promise<void> {
|
|
|
|
|
if (csrfCookieFetched) return;
|
|
|
|
|
await apiClient.get('/sanctum/csrf-cookie');
|
|
|
|
|
csrfCookieFetched = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Хелпер для обработки validation-error 422 от Laravel.
|
|
|
|
|
* Возвращает `{ field: [messages] }` или null.
|
|
|
|
|
*/
|
|
|
|
|
export function extractValidationErrors(error: unknown): Record<string, string[]> | null {
|
|
|
|
|
if (!axios.isAxiosError(error)) return null;
|
|
|
|
|
if (error.response?.status !== 422) return null;
|
|
|
|
|
const data = error.response.data as { errors?: Record<string, string[]> };
|
|
|
|
|
return data.errors ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Хелпер для general error-message (любая ошибка → human-readable строка).
|
|
|
|
|
*/
|
|
|
|
|
export function extractErrorMessage(error: unknown, fallback = 'Произошла ошибка. Попробуйте позже.'): string {
|
|
|
|
|
if (axios.isAxiosError(error)) {
|
|
|
|
|
const data = error.response?.data as { message?: string } | undefined;
|
|
|
|
|
if (data?.message) return data.message;
|
|
|
|
|
if (error.response?.status === 401) return 'Требуется вход.';
|
|
|
|
|
if (error.response?.status === 403) return 'Нет прав на это действие.';
|
|
|
|
|
if (error.response?.status === 500) return 'Внутренняя ошибка сервера.';
|
|
|
|
|
}
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
2026-05-08 20:49:47 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Хелпер для 429 Too Many Requests (ТЗ §22.4.4: 5 попыток / 15 мин).
|
|
|
|
|
* Возвращает retry_after в секундах или null для других ошибок.
|
|
|
|
|
*
|
|
|
|
|
* Backend кладёт `retry_after: number` в JSON и `Retry-After` в headers
|
|
|
|
|
* (см. AuthController::lockoutResponse).
|
|
|
|
|
*/
|
|
|
|
|
export function extractRateLimitRetry(error: unknown): number | null {
|
|
|
|
|
if (!axios.isAxiosError(error)) return null;
|
|
|
|
|
if (error.response?.status !== 429) return null;
|
|
|
|
|
const data = error.response.data as { retry_after?: number } | undefined;
|
|
|
|
|
if (typeof data?.retry_after === 'number') return data.retry_after;
|
|
|
|
|
const header = error.response.headers['retry-after'];
|
|
|
|
|
if (header) return parseInt(String(header), 10) || null;
|
|
|
|
|
return null;
|
|
|
|
|
}
|