Files
evening_detective_frontend/src/stores/auth.ts
T
2026-10-08 02:07:59 +07:00

277 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string | null>(null)
const refreshToken = ref<string | null>(null)
const user = ref<User | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
let refreshPromise: Promise<boolean> | 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<boolean> {
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,
}
})