Files
portal/app/resources/js/stores/auth.ts
T

172 lines
5.9 KiB
TypeScript
Raw Normal View History

import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import * as authApi from '../api/auth';
import type { AuthUser, LoginPayload, RegisterPayload, ResetPasswordPayload } from '../api/auth';
import { extractRateLimitRetry } from '../api/client';
/**
* Auth-store: состояние текущего user'а + auth-actions через Sanctum SPA.
*
* Использование:
* const auth = useAuthStore();
* await auth.login({ email, password }); // редирект на /dashboard или /2fa
* await auth.fetchMe(); // restore session при старте app
* if (auth.isAuthenticated) { ... }
* await auth.logout();
*
* Не входит:
* - Persist user в localStorage (session-cookie держит state на backend; при
* page-reload `fetchMe()` восстанавливает user). Если cookie expired — 401
* и redirect на /login через Vue Router auth-guard.
*/
export const useAuthStore = defineStore('auth', () => {
const user = ref<AuthUser | null>(null);
const loading = ref(false);
const requires2fa = ref(false);
/** Секунды до следующей разрешённой попытки login/2fa-verify (ТЗ §22.4.4). */
const lockoutSeconds = ref<number | null>(null);
const isAuthenticated = computed(() => user.value !== null);
async function login(payload: LoginPayload) {
loading.value = true;
lockoutSeconds.value = null;
try {
const response = await authApi.login(payload);
// При requires_2fa=true НЕ ставим user в state — иначе isAuthenticated
// станет true и auth-guard пустит на /dashboard минуя 2FA. Backend
// тоже НЕ создаёт session-auth до verifyTwoFactor.
if (response.requires_2fa) {
user.value = null;
requires2fa.value = true;
} else {
user.value = response.user;
requires2fa.value = false;
}
return response;
} catch (error) {
const retry = extractRateLimitRetry(error);
if (retry !== null) lockoutSeconds.value = retry;
throw error;
} finally {
loading.value = false;
}
}
async function register(payload: RegisterPayload) {
loading.value = true;
try {
const response = await authApi.register(payload);
user.value = response.user;
requires2fa.value = response.requires_2fa;
return response;
} finally {
loading.value = false;
}
}
async function requestPasswordReset(email: string) {
loading.value = true;
lockoutSeconds.value = null;
try {
const response = await authApi.forgotPassword(email);
return response;
} catch (error) {
const retry = extractRateLimitRetry(error);
if (retry !== null) lockoutSeconds.value = retry;
throw error;
} finally {
loading.value = false;
}
}
async function resetPassword(payload: ResetPasswordPayload) {
loading.value = true;
lockoutSeconds.value = null;
try {
const response = await authApi.resetPassword(payload);
return response;
} catch (error) {
const retry = extractRateLimitRetry(error);
if (retry !== null) lockoutSeconds.value = retry;
throw error;
} finally {
loading.value = false;
}
}
async function verifyTwoFactor(code: string) {
loading.value = true;
lockoutSeconds.value = null;
try {
const response = await authApi.verifyTwoFactor(code);
user.value = response.user;
requires2fa.value = false;
return response;
} catch (error) {
const retry = extractRateLimitRetry(error);
if (retry !== null) lockoutSeconds.value = retry;
throw error;
} finally {
loading.value = false;
}
}
async function useRecoveryCode(code: string) {
loading.value = true;
lockoutSeconds.value = null;
try {
const response = await authApi.useRecoveryCode(code);
user.value = response.user;
requires2fa.value = false;
return response;
} catch (error) {
const retry = extractRateLimitRetry(error);
if (retry !== null) lockoutSeconds.value = retry;
throw error;
} finally {
loading.value = false;
}
}
async function fetchMe(): Promise<AuthUser | null> {
try {
const fetched = await authApi.me();
user.value = fetched;
return fetched;
} catch {
user.value = null;
return null;
}
}
async function logout() {
// Логаут всегда успешен с точки зрения UI: даже если backend упал —
// клиент локально считается вышедшим. Иначе пользователь может остаться
// «залипшим» в авторизованном состоянии при сетевой ошибке.
try {
await authApi.logout();
} catch {
// ignore — backend всё равно очистит сессию по cookie-expiry.
}
user.value = null;
requires2fa.value = false;
}
return {
user,
loading,
requires2fa,
lockoutSeconds,
isAuthenticated,
login,
register,
verifyTwoFactor,
useRecoveryCode,
requestPasswordReset,
resetPassword,
fetchMe,
logout,
};
});