Files
portal/app/resources/js/api/client.ts
T

74 lines
3.2 KiB
TypeScript
Raw Normal View History

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;
}
/**
* Хелпер для 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;
}