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 { if (csrfCookieFetched) return; await apiClient.get('/sanctum/csrf-cookie'); csrfCookieFetched = true; } /** * Хелпер для обработки validation-error 422 от Laravel. * Возвращает `{ field: [messages] }` или null. */ export function extractValidationErrors(error: unknown): Record | null { if (!axios.isAxiosError(error)) return null; if (error.response?.status !== 422) return null; const data = error.response.data as { errors?: Record }; 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; } /** * Хелпер для 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; }