This commit is contained in:
2026-10-08 02:07:59 +07:00
parent 393207fe21
commit 84341a5e27
8 changed files with 494 additions and 111 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Вечерний детектив</title> <title>Вечерний детектив</title>
<script type="module" crossorigin src="/assets/index-D4Ouwe08.js"></script> <script type="module" crossorigin src="/assets/index-CHmzhHGh.js"></script>
<link rel="modulepreload" crossorigin href="/assets/client-BfLkiLXr.js"> <link rel="modulepreload" crossorigin href="/assets/client-BfLkiLXr.js">
<link rel="modulepreload" crossorigin href="/assets/light-CQqBJDIF.js"> <link rel="modulepreload" crossorigin href="/assets/light-CQqBJDIF.js">
<link rel="modulepreload" crossorigin href="/assets/light-BrwQlAEt.js"> <link rel="modulepreload" crossorigin href="/assets/light-BrwQlAEt.js">
+61
View File
@@ -0,0 +1,61 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { retryWithAuth } from '../authRetry.ts'
for (const scenario of [
{ name: '401 retries after refresh', error: 'unauthorized', refresh: true, attempts: 2 },
{ name: 'failed refresh does not retry', error: 'unauthorized', refresh: false, attempts: 1 },
{ name: '403 does not refresh', error: 'permission denied', refresh: true, attempts: 1 },
{ name: '500 does not refresh', error: 'server error', refresh: true, attempts: 1 },
{
name: 'network failure does not refresh',
error: 'Failed to fetch',
refresh: true,
attempts: 1,
},
]) {
test(scenario.name, async () => {
let attempts = 0
let refreshes = 0
const original = new Error(scenario.error)
const result = retryWithAuth(
async () => {
attempts++
if (attempts === 1) throw original
return 'ok'
},
async (failedToken) => {
assert.equal(failedToken, 'expired')
refreshes++
return scenario.refresh
},
() => 'expired',
)
if (scenario.attempts === 2) assert.equal(await result, 'ok')
else await assert.rejects(result, (err) => err === original)
assert.equal(attempts, scenario.attempts)
assert.equal(refreshes, scenario.error === 'unauthorized' ? 1 : 0)
})
}
test('second 401 is propagated without another refresh', async () => {
let attempts = 0
let refreshes = 0
await assert.rejects(
retryWithAuth(
async () => {
attempts++
throw new Error('unauthorized')
},
async () => {
refreshes++
return true
},
() => 'expired',
),
/unauthorized/,
)
assert.equal(attempts, 2)
assert.equal(refreshes, 1)
})
+14
View File
@@ -0,0 +1,14 @@
export async function retryWithAuth<T>(
request: () => Promise<T>,
refreshTokens: (failedToken?: string | null) => Promise<boolean>,
getToken: () => string | null,
): Promise<T> {
const token = getToken()
try {
return await request()
} catch (err) {
if (!(err instanceof Error) || err.message !== 'unauthorized') throw err
if (!(await refreshTokens(token))) throw err
return request()
}
}
+12 -30
View File
@@ -1,45 +1,27 @@
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { retryWithAuth } from './authRetry'
import { import {
createEveningDetectiveServerClient, createEveningDetectiveServerClient,
type EveningDetectiveServer, type EveningDetectiveServer,
} from './generated/crabs/evening_detective_server' } from './generated/crabs/evening_detective_server'
import { createHttpHandler } from './generated/crabs/evening_detective_server/client' import {
createHttpHandler,
type ExtraHeadersProvider,
} from './generated/crabs/evening_detective_server/client'
export function getAuthClient() { export function getAuthClient(getExtraHeaders?: ExtraHeadersProvider) {
const authStore = useAuthStore() const authStore = useAuthStore()
return buildAuthClient(authStore.refreshTokenAction, authStore.getToken) return buildAuthClient(authStore.refreshTokenAction, authStore.getToken, getExtraHeaders)
} }
function buildAuthClient( function buildAuthClient(
refreshTokens: () => Promise<boolean>, refreshTokens: (failedToken?: string | null) => Promise<boolean>,
getToken: () => string | null, getToken: () => string | null,
getExtraHeaders?: ExtraHeadersProvider,
): EveningDetectiveServer { ): EveningDetectiveServer {
return createEveningDetectiveServerClient( const handler = createHttpHandler(getToken, getExtraHeaders)
createAuthHttpHandler(refreshTokens, createHttpHandler(getToken)), return createEveningDetectiveServerClient((request) =>
retryWithAuth(() => handler(request), refreshTokens, getToken),
) )
} }
function createAuthHttpHandler(
refreshTokens: () => Promise<boolean>,
handler: (request: { path: string; method: string; body: string | null }) => Promise<unknown>,
): (request: { path: string; method: string; body: string | null }) => Promise<unknown> {
return async (request: { path: string; method: string; body: string | null }) => {
let response: unknown
try {
response = await handler(request)
} catch {
await refreshTokens()
response = await handler(request)
}
// Gateway возвращает gRPC-ошибки как JSON даже при HTTP 403/400/500.
if (response && typeof response === 'object' && 'code' in response && response.code !== 0) {
throw new Error(
'message' in response && typeof response.message === 'string'
? response.message
: 'Ошибка запроса',
)
}
return response
}
}
+17 -26
View File
@@ -1,5 +1,6 @@
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { retryWithAuth } from './authRetry'
import { getURL } from './generated/crabs/evening_detective_server/client' import { getURL } from './generated/crabs/evening_detective_server/client'
export type DownloadFileResult = { export type DownloadFileResult = {
@@ -18,33 +19,23 @@ export function getBinaryClient() {
return buildBinaryClient(authStore.refreshTokenAction, authStore.getToken) return buildBinaryClient(authStore.refreshTokenAction, authStore.getToken)
} }
function buildBinaryClient(refreshTokens: () => Promise<boolean>, getToken: () => string | null) { function buildBinaryClient(
refreshTokens: (failedToken?: string | null) => Promise<boolean>,
getToken: () => string | null,
) {
async function request(path: string, init: RequestInit): Promise<Response> { async function request(path: string, init: RequestInit): Promise<Response> {
const headers: Record<string, string> = {} return retryWithAuth(
const token = getToken() async () => {
if (token) { const headers = new Headers(init.headers)
headers['Authorization'] = `Bearer ${token}` const token = getToken()
} if (token) headers.set('Authorization', `Bearer ${token}`)
const response = await fetch(`${getURL()}/${path}`, { ...init, headers })
let response = await fetch(`${getURL()}/${path}`, { if (response.status === 401) throw new Error('unauthorized')
...init, return response
headers: { ...headers, ...(init.headers as Record<string, string>) }, },
}) refreshTokens,
getToken,
// Токен протух — обновляем и повторяем запрос один раз. )
if (response.status == 401) {
await refreshTokens()
const newToken = getToken()
if (newToken) {
headers['Authorization'] = `Bearer ${newToken}`
}
response = await fetch(`${getURL()}/${path}`, {
...init,
headers: { ...headers, ...(init.headers as Record<string, string>) },
})
}
return response
} }
async function download(path: string, fallbackName: string): Promise<DownloadFileResult> { async function download(path: string, fallbackName: string): Promise<DownloadFileResult> {
+5 -3
View File
@@ -2,7 +2,7 @@
import { MenuOutlined } from '@vicons/material' import { MenuOutlined } from '@vicons/material'
import { Icon } from '@vicons/utils' import { Icon } from '@vicons/utils'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { ref } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client' import { getAuthClient } from '@/api/auth_client'
@@ -19,15 +19,17 @@ const permissions = ref<string[]>([])
async function getPermissions() { async function getPermissions() {
permissions.value = [] permissions.value = []
if (!authStore.user) return
const token = authStore.getToken()
try { try {
const res = await client.GetPermissions({}) const res = await client.GetPermissions({})
permissions.value = res.permissions || [] if (token === authStore.getToken()) permissions.value = res.permissions || []
} catch (err) { } catch (err) {
message.error('Не удалось загрузить права доступа') message.error('Не удалось загрузить права доступа')
console.error('Failed to load permissions:', err) console.error('Failed to load permissions:', err)
} }
} }
getPermissions() watch(() => authStore.user, getPermissions, { immediate: true })
function hasPermission(permission: string): boolean { function hasPermission(permission: string): boolean {
return permissions.value.includes(permission) return permissions.value.includes(permission)
+289
View File
@@ -0,0 +1,289 @@
import assert from 'node:assert/strict'
import { after, beforeEach, test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { createPinia, setActivePinia } from 'pinia'
import { createServer } from 'vite'
const server = await createServer({
configFile: false,
root: fileURLToPath(new URL('../../..', import.meta.url)),
resolve: { alias: { '@': fileURLToPath(new URL('../..', import.meta.url)) } },
server: { middlewareMode: true, hmr: false, ws: false },
})
const { useAuthStore } = (await server.ssrLoadModule(
'/src/stores/auth.ts',
)) as typeof import('../auth')
const { getAuthClient } = (await server.ssrLoadModule(
'/src/api/auth_client.ts',
)) as typeof import('../../api/auth_client')
const { getBinaryClient } = (await server.ssrLoadModule(
'/src/api/binary_client.ts',
)) as typeof import('../../api/binary_client')
const originalFetch = globalThis.fetch
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
after(async () => {
globalThis.fetch = originalFetch
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator)
await server.close()
})
class MemoryStorage {
values = new Map<string, string>()
getItem(key: string) {
return this.values.get(key) ?? null
}
setItem(key: string, value: string) {
this.values.set(key, value)
}
removeItem(key: string) {
this.values.delete(key)
}
}
let storage: MemoryStorage
let listeners: Array<(event: { key: string | null; storageArea: MemoryStorage }) => void>
let lockTail: Promise<unknown>
beforeEach(() => {
storage = new MemoryStorage()
storage.setItem('accessToken', 'expired')
storage.setItem('refreshToken', 'refresh-old')
listeners = []
lockTail = Promise.resolve()
Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: storage })
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
addEventListener: (_name: string, listener: (typeof listeners)[number]) =>
listeners.push(listener),
},
})
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: (_name: string, action: () => Promise<boolean>) => {
const result = lockTail.then(action)
lockTail = result.catch(() => {})
return result
},
},
},
})
setActivePinia(createPinia())
})
function storeWithTokens() {
const store = useAuthStore()
store.accessToken = storage.getItem('accessToken')
store.refreshToken = storage.getItem('refreshToken')
return store
}
function json(data: unknown, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => {
resolve = done
})
return { promise, resolve }
}
for (const withLocks of [true, false]) {
test(`simultaneous JSON and binary 401 share one refresh (locks=${withLocks})`, async () => {
if (!withLocks)
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: {} })
const store = storeWithTokens()
let refreshes = 0
globalThis.fetch = async (url, init) => {
if (String(url).endsWith('/refresh')) {
refreshes++
assert.equal(new Headers(init?.headers).get('Authorization'), null)
assert.equal(JSON.parse(String(init?.body)).refreshToken, 'refresh-old')
return json({ accessToken: 'fresh', refreshToken: 'refresh-new' })
}
if (new Headers(init?.headers).get('Authorization') !== 'Bearer fresh') return json({}, 401)
if (String(url).endsWith('/archive')) return new Response('zip')
return json({ user: { ID: 1, username: 'Пользователь', roles: ['admin'] } })
}
await Promise.all([
store.fetchUser(),
store.fetchUser(),
getBinaryClient().downloadScenarioArchive(1),
])
assert.equal(refreshes, 1)
assert.equal(store.username, 'Пользователь')
assert.equal(store.hasRole('admin'), true)
assert.equal(storage.getItem('accessToken'), 'fresh')
// A delayed 401 for the previous token must not rotate the new token again.
assert.equal(await store.refreshTokenAction('expired'), true)
assert.equal(refreshes, 1)
})
}
test('reload restores expired tokens, user and roles', async () => {
const store = useAuthStore()
const fetched = deferred<void>()
globalThis.fetch = async (url, init) => {
if (String(url).endsWith('/refresh'))
return json({ accessToken: 'fresh', refreshToken: 'refresh-new' })
if (new Headers(init?.headers).get('Authorization') !== 'Bearer fresh') return json({}, 401)
fetched.resolve()
return json({ user: { ID: 1, username: 'Пользователь', roles: ['admin'] } })
}
store.initFromStorage()
await fetched.promise
await store.fetchUser()
assert.equal(store.username, 'Пользователь')
assert.equal(store.hasRole('admin'), true)
})
for (const failure of ['401', '500', 'network']) {
test(`refresh ${failure} ${failure === '401' ? 'clears' : 'preserves'} session`, async () => {
const store = storeWithTokens()
globalThis.fetch = async () => {
if (failure === 'network') throw new TypeError('Failed to fetch')
return json({ code: failure === '401' ? 16 : 13, message: 'server error' }, Number(failure))
}
if (failure === '401') assert.equal(await store.refreshTokenAction(), false)
else await assert.rejects(store.refreshTokenAction())
assert.equal(storage.getItem('refreshToken'), failure === '401' ? null : 'refresh-old')
assert.equal(store.isAuthenticated, failure !== '401')
})
}
test('logout while refresh is in flight prevents restoring the session', async () => {
const store = storeWithTokens()
const started = deferred<void>()
const response = deferred<Response>()
globalThis.fetch = async () => {
started.resolve()
return response.promise
}
const refreshing = store.refreshTokenAction()
await started.promise
await store.logout()
response.resolve(json({ accessToken: 'fresh', refreshToken: 'refresh-new' }))
assert.equal(await refreshing, false)
assert.equal(store.isAuthenticated, false)
assert.equal(storage.getItem('refreshToken'), null)
})
test('two tabs serialize refresh and reuse the stored result', async () => {
const first = storeWithTokens()
setActivePinia(createPinia())
const second = storeWithTokens()
let refreshes = 0
globalThis.fetch = async () => {
refreshes++
return json({ accessToken: 'fresh', refreshToken: 'refresh-new' })
}
const results = await Promise.all([first.refreshTokenAction(), second.refreshTokenAction()])
assert.deepEqual(results, [true, true])
assert.equal(refreshes, 1)
assert.equal(second.accessToken, 'fresh')
assert.equal(second.refreshToken, 'refresh-new')
})
test('storage events synchronize tokens and logout', async () => {
const store = useAuthStore()
globalThis.fetch = async () =>
json({ user: { ID: 1, username: 'Пользователь', roles: ['admin'] } })
store.initFromStorage()
await store.fetchUser()
storage.setItem('refreshToken', 'refresh-new')
storage.setItem('accessToken', 'fresh')
for (const listener of listeners) listener({ key: 'accessToken', storageArea: storage })
await store.fetchUser()
assert.equal(store.accessToken, 'fresh')
assert.equal(store.username, 'Пользователь')
storage.removeItem('accessToken')
storage.removeItem('refreshToken')
for (const listener of listeners) listener({ key: 'accessToken', storageArea: storage })
assert.equal(store.isAuthenticated, false)
assert.equal(store.user, null)
})
test('login omits stale authorization and account deletion retains password on retry', async () => {
const store = storeWithTokens()
let deletions = 0
globalThis.fetch = async (url, init) => {
const headers = new Headers(init?.headers)
const path = String(url)
if (path.endsWith('/login')) {
assert.equal(headers.get('Authorization'), null)
return json({ accessToken: 'expired', refreshToken: 'refresh-old' })
}
if (path.endsWith('/refresh'))
return json({ accessToken: 'fresh', refreshToken: 'refresh-new' })
if (init?.method === 'DELETE') {
assert.equal(headers.get('X-Password'), 'password')
deletions++
return json({}, deletions === 1 ? 401 : 200)
}
return json({ user: { ID: 1, username: 'Пользователь', roles: [] } })
}
assert.equal(await store.login('test@example.invalid', 'password'), true)
assert.equal(await store.deleteAccount('password'), true)
assert.equal(deletions, 2)
assert.equal(store.isAuthenticated, false)
})
for (const status of [403, 500]) {
test(`JSON ${status} is not refreshed or retried`, async () => {
storeWithTokens()
let requests = 0
globalThis.fetch = async () => {
requests++
return json({ code: 7, message: 'denied' }, status)
}
await assert.rejects(getAuthClient().GetMe({}), /denied/)
assert.equal(requests, 1)
})
}
for (const operation of ['login', 'fetchUser']) {
test(`late ${operation} response cannot restore the user after logout`, async () => {
const store = storeWithTokens()
const started = deferred<void>()
const response = deferred<Response>()
globalThis.fetch = async () => {
started.resolve()
return response.promise
}
const pending =
operation === 'login' ? store.login('test@example.invalid', 'password') : store.fetchUser()
await started.promise
await store.logout()
response.resolve(
json({
accessToken: 'fresh',
refreshToken: 'refresh-new',
user: { ID: 1, username: 'Old user', roles: ['admin'] },
}),
)
await pending
assert.equal(store.isAuthenticated, false)
assert.equal(store.user, null)
assert.equal(storage.getItem('accessToken'), null)
})
}
test('successful recovery clears the previous temporary error', async () => {
const store = storeWithTokens()
globalThis.fetch = async () => {
throw new TypeError('Failed to fetch')
}
await store.fetchUser()
assert.equal(store.error, 'Failed to fetch')
globalThis.fetch = async () => json({ user: { ID: 1, username: 'Recovered', roles: ['admin'] } })
await store.fetchUser()
assert.equal(store.error, null)
assert.equal(store.username, 'Recovered')
})
+95 -51
View File
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { getAuthClient } from '@/api/auth_client'
import { type User } from '@/api/generated/crabs/evening_detective_server' import { type User } from '@/api/generated/crabs/evening_detective_server'
import { buildClient } from '@/api/generated/crabs/evening_detective_server/client' import { buildClient } from '@/api/generated/crabs/evening_detective_server/client'
@@ -12,6 +13,10 @@ export const useAuthStore = defineStore('auth', () => {
const isLoading = ref(false) const isLoading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
let refreshPromise: Promise<boolean> | null = null
let sessionVersion = 0
let isStorageListening = false
// ===== GETTERS ===== // ===== GETTERS =====
const isAuthenticated = computed(() => !!accessToken.value) const isAuthenticated = computed(() => !!accessToken.value)
const username = computed(() => user.value?.username || '') const username = computed(() => user.value?.username || '')
@@ -20,17 +25,49 @@ export const useAuthStore = defineStore('auth', () => {
// ===== ACTIONS ===== // ===== ACTIONS =====
// Инициализация из localStorage при загрузке // Инициализация из 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() { function initFromStorage() {
const token = localStorage.getItem('accessToken') readTokens()
const refresh = localStorage.getItem('refreshToken') if (!isStorageListening) {
window.addEventListener('storage', (event) => {
if (token) { if (
accessToken.value = token event.storageArea !== localStorage ||
refreshToken.value = refresh (event.key !== 'accessToken' && event.key !== null)
)
// Получаем данные пользователя return
fetchUser() 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 { function getToken(): string | null {
@@ -41,18 +78,17 @@ export const useAuthStore = defineStore('auth', () => {
async function fetchUser() { async function fetchUser() {
if (!accessToken.value) return if (!accessToken.value) return
const version = sessionVersion
try { try {
const client = buildClient(getToken) const response = await getAuthClient().GetMe({})
const response = await client.GetMe({})
if (response.user) { if (response.user && version === sessionVersion && accessToken.value) {
user.value = response.user user.value = response.user
error.value = null
} }
} catch (err) { } catch (err) {
console.error('Failed to fetch user:', err) if (version === sessionVersion) {
// Если 401 — токен протух error.value = err instanceof Error ? err.message : 'Не удалось восстановить сессию'
if (err instanceof Error && err.message.includes('401')) {
await logout()
} }
} }
} }
@@ -62,20 +98,14 @@ export const useAuthStore = defineStore('auth', () => {
isLoading.value = true isLoading.value = true
error.value = null error.value = null
const version = ++sessionVersion
try { try {
const client = buildClient(getToken) const client = buildClient()
const response = await client.Login({ email, password }) const response = await client.Login({ email, password })
// Сохраняем токены if (response.accessToken && version === sessionVersion) {
if (response.accessToken) { user.value = null
accessToken.value = response.accessToken saveTokens(response.accessToken, response.refreshToken)
refreshToken.value = response.refreshToken || null
// Сохраняем в localStorage
localStorage.setItem('accessToken', response.accessToken)
if (response.refreshToken) {
localStorage.setItem('refreshToken', response.refreshToken)
}
// Получаем данные пользователя // Получаем данные пользователя
await fetchUser() await fetchUser()
@@ -93,32 +123,45 @@ export const useAuthStore = defineStore('auth', () => {
} }
// Обновление токена // Обновление токена
async function refreshTokenAction() { function refreshTokenAction(failedToken: string | null = accessToken.value): Promise<boolean> {
if (!refreshToken.value) return false if (refreshPromise) return refreshPromise
const version = sessionVersion
try { const token = refreshToken.value
const client = buildClient(getToken) const refresh = async () => {
const response = await client.Refresh({ if (version !== sessionVersion) return false
refreshToken: refreshToken.value, readTokens()
}) if (!accessToken.value || !refreshToken.value) {
await logout()
if (!response.accessToken) {
return false return false
} }
if (accessToken.value !== failedToken || refreshToken.value !== token) return true
// Обновляем токены try {
accessToken.value = response.accessToken const response = await buildClient().Refresh({ refreshToken: token || undefined })
refreshToken.value = response.refreshToken || null if (version !== sessionVersion || localStorage.getItem('refreshToken') !== token)
return false
localStorage.setItem('accessToken', response.accessToken) if (!response.accessToken || !response.refreshToken) {
if (response.refreshToken) { throw new Error('Сервер не вернул токены сессии')
localStorage.setItem('refreshToken', response.refreshToken) }
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
} }
return true
} catch {
return false
} }
refreshPromise = (
navigator.locks
? navigator.locks.request('evening-detective-auth-refresh', refresh)
: refresh()
).finally(() => {
refreshPromise = null
})
return refreshPromise
} }
// Регистрация // Регистрация
@@ -151,7 +194,7 @@ export const useAuthStore = defineStore('auth', () => {
try { try {
// Пароль передаётся HTTP-заголовком X-Password, а не в URL или body // Пароль передаётся HTTP-заголовком X-Password, а не в URL или body
const client = buildClient(getToken, () => ({ 'X-Password': password })) const client = getAuthClient(() => ({ 'X-Password': password }))
await client.DeleteAccount({}) await client.DeleteAccount({})
await logout() await logout()
@@ -184,13 +227,14 @@ export const useAuthStore = defineStore('auth', () => {
// Выход // Выход
async function logout() { async function logout() {
sessionVersion++
accessToken.value = null accessToken.value = null
refreshToken.value = null refreshToken.value = null
user.value = null user.value = null
error.value = null error.value = null
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken') localStorage.removeItem('refreshToken')
localStorage.removeItem('accessToken')
} }
// Проверка ролей // Проверка ролей