import { defineStore } from 'pinia' import { computed, ref } from 'vue' import { getAuthClient } from '@/api/auth_client' import { type User } from '@/api/generated/crabs/evening_detective_server' import { buildClient } from '@/api/generated/crabs/evening_detective_server/client' export const useAuthStore = defineStore('auth', () => { // ===== STATE ===== const accessToken = ref(null) const refreshToken = ref(null) const user = ref(null) const isLoading = ref(false) const error = ref(null) let refreshPromise: Promise | null = null let sessionVersion = 0 let isStorageListening = false // ===== GETTERS ===== const isAuthenticated = computed(() => !!accessToken.value) const username = computed(() => user.value?.username || '') const userRoles = computed(() => user.value?.roles || []) // ===== ACTIONS ===== // Инициализация из localStorage при загрузке function readTokens() { accessToken.value = localStorage.getItem('accessToken') refreshToken.value = localStorage.getItem('refreshToken') } function saveTokens(access: string, refresh: string | undefined) { accessToken.value = access refreshToken.value = refresh || null if (refresh) localStorage.setItem('refreshToken', refresh) else localStorage.removeItem('refreshToken') localStorage.setItem('accessToken', access) } function initFromStorage() { readTokens() if (!isStorageListening) { window.addEventListener('storage', (event) => { if ( event.storageArea !== localStorage || (event.key !== 'accessToken' && event.key !== null) ) return const sync = () => { sessionVersion++ readTokens() user.value = null error.value = null void fetchUser() } // Дождаться уже начатого обновления; выход обрабатывается сразу. if ( refreshPromise && localStorage.getItem('accessToken') && localStorage.getItem('refreshToken') ) { void refreshPromise.then(sync, sync) } else { sync() } }) isStorageListening = true } void fetchUser() } function getToken(): string | null { return accessToken.value } // Получение данных пользователя async function fetchUser() { if (!accessToken.value) return const version = sessionVersion try { const response = await getAuthClient().GetMe({}) if (response.user && version === sessionVersion && accessToken.value) { user.value = response.user error.value = null } } catch (err) { if (version === sessionVersion) { error.value = err instanceof Error ? err.message : 'Не удалось восстановить сессию' } } } // Логин async function login(email: string, password: string) { isLoading.value = true error.value = null const version = ++sessionVersion try { const client = buildClient() const response = await client.Login({ email, password }) if (response.accessToken && version === sessionVersion) { user.value = null saveTokens(response.accessToken, response.refreshToken) // Получаем данные пользователя await fetchUser() return true } return false } catch (err) { error.value = err instanceof Error ? err.message : 'Login failed' return false } finally { isLoading.value = false } } // Обновление токена function refreshTokenAction(failedToken: string | null = accessToken.value): Promise { if (refreshPromise) return refreshPromise const version = sessionVersion const token = refreshToken.value const refresh = async () => { if (version !== sessionVersion) return false readTokens() if (!accessToken.value || !refreshToken.value) { await logout() return false } if (accessToken.value !== failedToken || refreshToken.value !== token) return true try { const response = await buildClient().Refresh({ refreshToken: token || undefined }) if (version !== sessionVersion || localStorage.getItem('refreshToken') !== token) return false if (!response.accessToken || !response.refreshToken) { throw new Error('Сервер не вернул токены сессии') } saveTokens(response.accessToken, response.refreshToken) return true } catch (err) { if (version !== sessionVersion || localStorage.getItem('refreshToken') !== token) return false if (err instanceof Error && err.message === 'unauthorized') { await logout() return false } throw err } } refreshPromise = ( navigator.locks ? navigator.locks.request('evening-detective-auth-refresh', refresh) : refresh() ).finally(() => { refreshPromise = null }) return refreshPromise } // Регистрация async function signup( username: string, email: string, acceptTerms: boolean, acceptPrivacy: boolean, ) { isLoading.value = true error.value = null try { const client = buildClient(getToken) await client.Signup({ username, email, acceptTerms, acceptPrivacy }) return true } catch (err) { error.value = err instanceof Error ? err.message : 'Signup failed' return false } finally { isLoading.value = false } } // Удаление аккаунта (подтверждение владельца паролем, ст. 21 152-ФЗ) async function deleteAccount(password: string) { isLoading.value = true error.value = null try { // Пароль передаётся HTTP-заголовком X-Password, а не в URL или body const client = getAuthClient(() => ({ 'X-Password': password })) await client.DeleteAccount({}) await logout() return true } catch (err) { error.value = err instanceof Error ? err.message : 'Account deletion failed' return false } finally { isLoading.value = false } } // Восстановление пароля async function refreshPassword(email: string) { isLoading.value = true error.value = null try { const client = buildClient(getToken) await client.RefreshPassword({ email }) return true } catch (err) { error.value = err instanceof Error ? err.message : 'Password reset failed' return false } finally { isLoading.value = false } } // Выход async function logout() { sessionVersion++ accessToken.value = null refreshToken.value = null user.value = null error.value = null localStorage.removeItem('refreshToken') localStorage.removeItem('accessToken') } // Проверка ролей function hasRole(role: string): boolean { return userRoles.value.includes(role) } function hasAnyRole(roles: string[]): boolean { return roles.some((role) => userRoles.value.includes(role)) } // ===== RETURN ===== return { // State accessToken, refreshToken, user, isLoading, error, // Getters isAuthenticated, username, userRoles, // Actions initFromStorage, getToken, login, signup, deleteAccount, refreshPassword, logout, fetchUser, refreshTokenAction, hasRole, hasAnyRole, } })