fix docs
This commit is contained in:
File diff suppressed because one or more lines are too long
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<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-D1qd5k5N.js"></script>
|
<script type="module" crossorigin src="/assets/index-D4qEPrTa.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CneFMUxK.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CneFMUxK.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
AdminLoginRequest,
|
AdminLoginRequest,
|
||||||
AdminLoginResponse,
|
AdminLoginResponse,
|
||||||
AdminMeResponse,
|
AdminMeResponse,
|
||||||
|
AdminRefreshResponse,
|
||||||
CreateOrganizationRequest,
|
CreateOrganizationRequest,
|
||||||
CreateWalletsResponse,
|
CreateWalletsResponse,
|
||||||
WalletResponse,
|
WalletResponse,
|
||||||
@@ -25,6 +26,22 @@ export const adminTokenStore = {
|
|||||||
clear: () => { adminToken = null },
|
clear: () => { adminToken = null },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The refresh token is body-based (sent in the `/v1/auth/refresh` request body),
|
||||||
|
// so it must persist across reloads to restore a session. localStorage-backed.
|
||||||
|
const ADMIN_REFRESH_KEY = 'admin_refresh_token'
|
||||||
|
|
||||||
|
export const adminRefreshStore = {
|
||||||
|
get: (): string | null => {
|
||||||
|
try { return localStorage.getItem(ADMIN_REFRESH_KEY) } catch { return null }
|
||||||
|
},
|
||||||
|
set: (token: string) => {
|
||||||
|
try { localStorage.setItem(ADMIN_REFRESH_KEY, token) } catch { /* ignore */ }
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
try { localStorage.removeItem(ADMIN_REFRESH_KEY) } catch { /* ignore */ }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
async function doAdminRequest<T>(
|
async function doAdminRequest<T>(
|
||||||
path: string,
|
path: string,
|
||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
@@ -60,17 +77,27 @@ async function doAdminRequest<T>(
|
|||||||
return data as T
|
return data as T
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh by analogy with the main auth service: HttpOnly refresh cookie -> fresh access token.
|
// Body-based refresh: the API expects the refresh token in the request body and
|
||||||
// Exact path is isolated here — adjust if the backend differs (e.g. /v1/jwt/refresh).
|
// returns a fresh access + refresh token pair (the refresh token rotates).
|
||||||
export async function refreshAdminToken(): Promise<string> {
|
export async function refreshAdminToken(): Promise<string> {
|
||||||
|
const refreshToken = adminRefreshStore.get()
|
||||||
|
if (!refreshToken) throw new Error('Unauthorized')
|
||||||
|
|
||||||
const res = await fetch(`${ADMIN_API_URL}/v1/auth/refresh`, {
|
const res = await fetch(`${ADMIN_API_URL}/v1/auth/refresh`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error('Unauthorized')
|
if (!res.ok) {
|
||||||
const data = await res.json()
|
adminRefreshStore.clear()
|
||||||
|
adminTokenStore.clear()
|
||||||
|
throw new Error('Unauthorized')
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as AdminRefreshResponse
|
||||||
if (data.access_token) adminTokenStore.set(data.access_token)
|
if (data.access_token) adminTokenStore.set(data.access_token)
|
||||||
return (data.access_token ?? true) as string
|
if (data.refresh_token) adminRefreshStore.set(data.refresh_token)
|
||||||
|
return data.access_token
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function adminLogin(payload: AdminLoginRequest): Promise<AdminLoginResponse> {
|
export async function adminLogin(payload: AdminLoginRequest): Promise<AdminLoginResponse> {
|
||||||
@@ -80,6 +107,7 @@ export async function adminLogin(payload: AdminLoginRequest): Promise<AdminLogin
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
if (data.access_token) adminTokenStore.set(data.access_token)
|
if (data.access_token) adminTokenStore.set(data.access_token)
|
||||||
|
if (data.refresh_token) adminRefreshStore.set(data.refresh_token)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +120,7 @@ export async function adminLogout(): Promise<void> {
|
|||||||
await doAdminRequest<unknown>('/v1/auth/logout', { method: 'POST' }, false)
|
await doAdminRequest<unknown>('/v1/auth/logout', { method: 'POST' }, false)
|
||||||
} finally {
|
} finally {
|
||||||
adminTokenStore.clear()
|
adminTokenStore.clear()
|
||||||
|
adminRefreshStore.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { refreshAdminToken } from '../api/adminApi'
|
import { getAdminMe } from '../api/adminApi'
|
||||||
|
|
||||||
export const ADMIN_AUTH_QUERY_KEY = ['admin-auth']
|
export const ADMIN_AUTH_QUERY_KEY = ['admin-auth']
|
||||||
|
|
||||||
export function useAdminAuth(): { isAuthenticated: boolean; isLoading: boolean } {
|
export function useAdminAuth(): { isAuthenticated: boolean; isLoading: boolean } {
|
||||||
|
// `getAdminMe` is the real "is the session valid" check. It runs through
|
||||||
|
// `doAdminRequest` with retry, so a 401 transparently triggers a body-based
|
||||||
|
// refresh (using the persisted refresh token) and replays the request.
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ADMIN_AUTH_QUERY_KEY,
|
queryKey: ADMIN_AUTH_QUERY_KEY,
|
||||||
queryFn: refreshAdminToken,
|
queryFn: getAdminMe,
|
||||||
retry: false,
|
retry: false,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: Infinity,
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ export function useAdminLogin() {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: adminLogin,
|
mutationFn: adminLogin,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
// The token is already stored by adminLogin; write it straight into the
|
// Tokens are already stored by adminLogin. Seed the gate's cache with the
|
||||||
// gate's query cache so we flip to "authenticated" without re-hitting /refresh.
|
// AdminMeResponse-shaped profile (login response carries all these fields)
|
||||||
queryClient.setQueryData(ADMIN_AUTH_QUERY_KEY, data.access_token)
|
// so we flip to "authenticated" without an extra /auth/me round-trip.
|
||||||
|
queryClient.setQueryData(ADMIN_AUTH_QUERY_KEY, {
|
||||||
|
id: data.id,
|
||||||
|
login: data.login,
|
||||||
|
first_name: data.first_name,
|
||||||
|
last_name: data.last_name,
|
||||||
|
role: data.role,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface AdminLoginRequest {
|
|||||||
|
|
||||||
export interface AdminLoginResponse {
|
export interface AdminLoginResponse {
|
||||||
access_token: string
|
access_token: string
|
||||||
|
refresh_token: string
|
||||||
token_type: string
|
token_type: string
|
||||||
id: string
|
id: string
|
||||||
login: string
|
login: string
|
||||||
@@ -13,6 +14,12 @@ export interface AdminLoginResponse {
|
|||||||
role: string
|
role: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminRefreshResponse {
|
||||||
|
access_token: string
|
||||||
|
refresh_token: string
|
||||||
|
token_type?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminMeResponse {
|
export interface AdminMeResponse {
|
||||||
id: string
|
id: string
|
||||||
login: string
|
login: string
|
||||||
|
|||||||
Reference in New Issue
Block a user