generated from VLADIMIR/template_frontend
277 lines
7.6 KiB
TypeScript
277 lines
7.6 KiB
TypeScript
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,
|
||
}
|
||
})
|