Files
portal/app/resources/js/api/auth.ts
T
Дмитрий f55b91cfa4 phase2(notifications-stage3): NotificationsTab schema-aligned + prefs API
Закрывает архитектурное расхождение v1.28 — Tab сохранял prefs только
локально без API. Backend events не совпадали с handoff'ом.

Backend:
- PATCH /api/auth/me/notification-preferences под auth:sanctum.
- Replace-семантика: незадекларированные events/channels отбрасываются.
- userResource расширен: notification_preferences + sound_enabled.
- UserFactory с schema-default JSON (Eloquent не перечитывает после INSERT,
  DB-DEFAULT JSONB виден как null без явного override).
- Pest +10: 401 / replace / неизвестные events/channels отбрасываются /
  422 без prefs / sound_enabled опционален / bool-cast 1/'1' / replace-
  семантика (отсутствующие events исчезают).

Frontend:
- api/auth.ts: типы NotificationChannel/EventKey/Preferences +
  updateNotificationPreferences helper. AuthUser получил optional поля.
- NotificationsTab.vue переписан под schema:
  8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/
  invoice_paid/new_device_login/marketing) × 3 канала (inapp/push/email,
  НЕ sms). Sync-init prefs (без onMounted — иначе v-if блокирует рендер
  и тесты mount-then-find падают). dirty через computed-сравнение с
  originalPrefs snapshot. save async + success/error alerts.
- SettingsView.spec.ts: legacy event-имена → schema-aligned.
- Vitest +10: 8 schema events / 3 channels (НЕ sms) / legacy отсутствуют /
  читает prefs из user / save calls API + alerts / Отменить возвращает.

cspell-words: +prefs.
PHPStan baseline регенерирован.

Pest 315/315 (+10) за 36.73 сек, 1130 assertions.
Vitest 349/349 (+10) за 20.42 сек.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:41:35 +03:00

154 lines
4.6 KiB
TypeScript

import { apiClient, ensureCsrfCookie } from './client';
/**
* API-вызовы для AuthController (см. app/Http/Controllers/Api/AuthController.php).
*
* Все методы делают `ensureCsrfCookie()` перед POST'ами — Sanctum SPA flow.
*/
export type NotificationChannel = 'inapp' | 'push' | 'email';
export type NotificationEventKey =
| 'new_lead'
| 'reminder'
| 'low_balance'
| 'zero_balance'
| 'topup_success'
| 'invoice_paid'
| 'new_device_login'
| 'marketing';
export type NotificationPreferences = Partial<
Record<NotificationEventKey, Partial<Record<NotificationChannel, boolean>>>
>;
export interface AuthUser {
id: number;
email: string;
first_name: string | null;
last_name: string | null;
tenant_id: number;
totp_enabled: boolean;
last_login_at: string | null;
notification_preferences?: NotificationPreferences;
sound_enabled?: boolean;
}
export interface LoginPayload {
email: string;
password: string;
remember?: boolean;
}
export interface LoginResponse {
user: AuthUser;
requires_2fa: boolean;
}
export interface RegisterPayload {
email: string;
password: string;
accept_offer: boolean;
accept_pdn: boolean;
}
export async function login(payload: LoginPayload): Promise<LoginResponse> {
await ensureCsrfCookie();
const { data } = await apiClient.post<LoginResponse>('/api/auth/login', payload);
return data;
}
export async function register(payload: RegisterPayload): Promise<LoginResponse> {
await ensureCsrfCookie();
const { data } = await apiClient.post<LoginResponse>('/api/auth/register', payload);
return data;
}
export async function me(): Promise<AuthUser> {
const { data } = await apiClient.get<{ user: AuthUser }>('/api/auth/me');
return data.user;
}
export async function logout(): Promise<void> {
await ensureCsrfCookie();
await apiClient.post('/api/auth/logout');
}
export async function verifyTwoFactor(code: string): Promise<LoginResponse> {
await ensureCsrfCookie();
const { data } = await apiClient.post<LoginResponse>('/api/auth/2fa/verify', { code });
return data;
}
export interface RecoveryCodeResponse extends LoginResponse {
recovery_codes_remaining: number;
}
export async function useRecoveryCode(code: string): Promise<RecoveryCodeResponse> {
await ensureCsrfCookie();
const { data } = await apiClient.post<RecoveryCodeResponse>('/api/auth/2fa/recovery-use', { code });
return data;
}
export interface TwoFactorInitResponse {
secret: string;
qr_url: string;
}
export async function twoFactorInit(): Promise<TwoFactorInitResponse> {
await ensureCsrfCookie();
const { data } = await apiClient.post<TwoFactorInitResponse>('/api/2fa/init');
return data;
}
export async function twoFactorConfirm(code: string): Promise<{ recovery_codes: string[]; message: string }> {
await ensureCsrfCookie();
const { data } = await apiClient.post<{ recovery_codes: string[]; message: string }>('/api/2fa/confirm', { code });
return data;
}
export async function twoFactorDisable(password: string): Promise<{ message: string }> {
await ensureCsrfCookie();
const { data } = await apiClient.post<{ message: string }>('/api/2fa/disable', { password });
return data;
}
export async function twoFactorRegenerateRecoveryCodes(
password: string,
): Promise<{ recovery_codes: string[]; message: string }> {
await ensureCsrfCookie();
const { data } = await apiClient.post<{ recovery_codes: string[]; message: string }>(
'/api/2fa/regenerate-recovery-codes',
{ password },
);
return data;
}
export async function forgotPassword(email: string): Promise<{ message: string }> {
await ensureCsrfCookie();
const { data } = await apiClient.post<{ message: string }>('/api/auth/forgot', { email });
return data;
}
export interface ResetPasswordPayload {
token: string;
email: string;
password: string;
password_confirmation: string;
}
export async function resetPassword(payload: ResetPasswordPayload): Promise<{ message: string }> {
await ensureCsrfCookie();
const { data } = await apiClient.post<{ message: string }>('/api/auth/reset-password', payload);
return data;
}
export interface UpdateNotificationPreferencesPayload {
prefs: NotificationPreferences;
sound_enabled?: boolean;
}
export async function updateNotificationPreferences(payload: UpdateNotificationPreferencesPayload): Promise<AuthUser> {
await ensureCsrfCookie();
const { data } = await apiClient.patch<{ user: AuthUser }>('/api/auth/me/notification-preferences', payload);
return data.user;
}