75897b1636
- AuthController: RateLimiter::hit/clear на login + verifyTwoFactor по ключу email|ip / pending_user_id|ip - 429 + Retry-After header + JSON retry_after (lockoutResponse helper) - ТЗ §22.4.4: 5 попыток / 15 мин; success чистит throttle; inactive user тоже расходует попытки - extractRateLimitRetry в api/client.ts; auth-store.lockoutSeconds; v-alert в LoginView/TwoFactorView - Pest +6 в RateLimitTest.php (73/73 за 8.07с, 246 assertions) - Vitest +4 в auth-store + LoginView (149/149 за 12.31с) - Quirk: wrong-password в тестах ≥8 символов (LoginRequest::min:8) — иначе валидация падает до controller - Quirk: vi.mock api/client в auth-store.spec — иначе axios.isAxiosError в jsdom возвращает false для plain Error - TODO (отдельные коммиты): IP-lockout 10/час через auth_log + email при 3 неудачах - Регресс: lint+type+format OK; build 886ms; story:build 21/28 за 37.19с; Pint+Stan passed - CLAUDE.md v1.35→v1.36, реестр v1.44→v1.45 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
3.2 KiB
TypeScript
74 lines
3.2 KiB
TypeScript
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;
|
|
}
|