admin page
This commit is contained in:
103
src/features/admin/api/adminApi.ts
Normal file
103
src/features/admin/api/adminApi.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type {
|
||||
AdminLoginRequest,
|
||||
AdminLoginResponse,
|
||||
AdminMeResponse,
|
||||
CreateOrganizationRequest,
|
||||
Organization,
|
||||
OrganizationListResponse,
|
||||
} from '../model/types'
|
||||
|
||||
const ADMIN_API_URL = 'https://app.admin.elcsa.ru/api'
|
||||
|
||||
// In-memory admin access token — deliberately separate from the user `tokenStore`
|
||||
// so the two independent auth systems never collide. No CSRF on the admin API.
|
||||
let adminToken: string | null = null
|
||||
|
||||
export const adminTokenStore = {
|
||||
get: () => adminToken,
|
||||
set: (token: string) => { adminToken = token },
|
||||
clear: () => { adminToken = null },
|
||||
}
|
||||
|
||||
async function doAdminRequest<T>(
|
||||
path: string,
|
||||
options: RequestInit,
|
||||
allowRetry: boolean,
|
||||
): Promise<T> {
|
||||
const bearer = adminTokenStore.get()
|
||||
|
||||
const res = await fetch(`${ADMIN_API_URL}${path}`, {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.status === 401 && allowRetry) {
|
||||
try {
|
||||
await refreshAdminToken()
|
||||
return doAdminRequest<T>(path, options, false)
|
||||
} catch {
|
||||
adminTokenStore.clear()
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => null)
|
||||
if (!res.ok) throw data
|
||||
return data as T
|
||||
}
|
||||
|
||||
// Refresh by analogy with the main auth service: HttpOnly refresh cookie -> fresh access token.
|
||||
// Exact path is isolated here — adjust if the backend differs (e.g. /v1/jwt/refresh).
|
||||
export async function refreshAdminToken(): Promise<string> {
|
||||
const res = await fetch(`${ADMIN_API_URL}/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) throw new Error('Unauthorized')
|
||||
const data = await res.json()
|
||||
if (data.access_token) adminTokenStore.set(data.access_token)
|
||||
return (data.access_token ?? true) as string
|
||||
}
|
||||
|
||||
export async function adminLogin(payload: AdminLoginRequest): Promise<AdminLoginResponse> {
|
||||
const data = await doAdminRequest<AdminLoginResponse>(
|
||||
'/v1/auth/login',
|
||||
{ method: 'POST', body: JSON.stringify(payload) },
|
||||
false,
|
||||
)
|
||||
if (data.access_token) adminTokenStore.set(data.access_token)
|
||||
return data
|
||||
}
|
||||
|
||||
export function getAdminMe(): Promise<AdminMeResponse> {
|
||||
return doAdminRequest<AdminMeResponse>('/v1/auth/me', {}, true)
|
||||
}
|
||||
|
||||
export async function adminLogout(): Promise<void> {
|
||||
try {
|
||||
await doAdminRequest<unknown>('/v1/auth/logout', { method: 'POST' }, false)
|
||||
} finally {
|
||||
adminTokenStore.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export function getOrganizations(limit = 50, offset = 0): Promise<OrganizationListResponse> {
|
||||
return doAdminRequest<OrganizationListResponse>(
|
||||
`/v1/organizations?limit=${limit}&offset=${offset}`,
|
||||
{},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
export function createOrganization(payload: CreateOrganizationRequest): Promise<Organization> {
|
||||
return doAdminRequest<Organization>(
|
||||
'/v1/organizations',
|
||||
{ method: 'POST', body: JSON.stringify(payload) },
|
||||
true,
|
||||
)
|
||||
}
|
||||
16
src/features/admin/hooks/useAdminAuth.ts
Normal file
16
src/features/admin/hooks/useAdminAuth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { refreshAdminToken } from '../api/adminApi'
|
||||
|
||||
export const ADMIN_AUTH_QUERY_KEY = ['admin-auth']
|
||||
|
||||
export function useAdminAuth(): { isAuthenticated: boolean; isLoading: boolean } {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ADMIN_AUTH_QUERY_KEY,
|
||||
queryFn: refreshAdminToken,
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
gcTime: Infinity,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
return { isAuthenticated: !!data && !isError, isLoading }
|
||||
}
|
||||
13
src/features/admin/hooks/useAdminLogin.ts
Normal file
13
src/features/admin/hooks/useAdminLogin.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { adminLogin } from '../api/adminApi'
|
||||
import { ADMIN_AUTH_QUERY_KEY } from './useAdminAuth'
|
||||
|
||||
export function useAdminLogin() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: adminLogin,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ADMIN_AUTH_QUERY_KEY })
|
||||
},
|
||||
})
|
||||
}
|
||||
13
src/features/admin/hooks/useAdminLogout.ts
Normal file
13
src/features/admin/hooks/useAdminLogout.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { adminLogout } from '../api/adminApi'
|
||||
import { ADMIN_AUTH_QUERY_KEY } from './useAdminAuth'
|
||||
|
||||
export function useAdminLogout() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: adminLogout,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ADMIN_AUTH_QUERY_KEY })
|
||||
},
|
||||
})
|
||||
}
|
||||
13
src/features/admin/hooks/useCreateOrganization.ts
Normal file
13
src/features/admin/hooks/useCreateOrganization.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { createOrganization } from '../api/adminApi'
|
||||
import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations'
|
||||
|
||||
export function useCreateOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: createOrganization,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY })
|
||||
},
|
||||
})
|
||||
}
|
||||
11
src/features/admin/hooks/useOrganizations.ts
Normal file
11
src/features/admin/hooks/useOrganizations.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getOrganizations } from '../api/adminApi'
|
||||
|
||||
export const ORGANIZATIONS_QUERY_KEY = ['admin-organizations']
|
||||
|
||||
export function useOrganizations() {
|
||||
return useQuery({
|
||||
queryKey: ORGANIZATIONS_QUERY_KEY,
|
||||
queryFn: () => getOrganizations(),
|
||||
})
|
||||
}
|
||||
23
src/features/admin/index.ts
Normal file
23
src/features/admin/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export {
|
||||
adminLogin,
|
||||
adminLogout,
|
||||
getAdminMe,
|
||||
getOrganizations,
|
||||
createOrganization,
|
||||
refreshAdminToken,
|
||||
adminTokenStore,
|
||||
} from './api/adminApi'
|
||||
export type {
|
||||
AdminLoginRequest,
|
||||
AdminLoginResponse,
|
||||
AdminMeResponse,
|
||||
Organization,
|
||||
OrganizationListResponse,
|
||||
CreateOrganizationRequest,
|
||||
BankDetails,
|
||||
} from './model/types'
|
||||
export { useAdminAuth, ADMIN_AUTH_QUERY_KEY } from './hooks/useAdminAuth'
|
||||
export { useAdminLogin } from './hooks/useAdminLogin'
|
||||
export { useAdminLogout } from './hooks/useAdminLogout'
|
||||
export { useOrganizations, ORGANIZATIONS_QUERY_KEY } from './hooks/useOrganizations'
|
||||
export { useCreateOrganization } from './hooks/useCreateOrganization'
|
||||
67
src/features/admin/model/types.ts
Normal file
67
src/features/admin/model/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export interface AdminLoginRequest {
|
||||
login: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface AdminLoginResponse {
|
||||
access_token: string
|
||||
token_type: string
|
||||
id: string
|
||||
login: string
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
role: string
|
||||
}
|
||||
|
||||
export interface AdminMeResponse {
|
||||
id: string
|
||||
login: string
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
role: string
|
||||
}
|
||||
|
||||
export type BankDetails = Record<string, unknown>
|
||||
|
||||
export interface Organization {
|
||||
id: string
|
||||
user_id: string
|
||||
name: string
|
||||
short_name: string | null
|
||||
inn: string
|
||||
ogrn: string | null
|
||||
kpp: string | null
|
||||
legal_address: string | null
|
||||
actual_address: string | null
|
||||
bank_details: BankDetails | null
|
||||
contact_person: string | null
|
||||
contact_phone: string | null
|
||||
status: string
|
||||
kyc_verified: boolean
|
||||
kyc_verified_at: string | null
|
||||
has_wallets: boolean
|
||||
created_by: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface OrganizationListResponse {
|
||||
items: Organization[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface CreateOrganizationRequest {
|
||||
email: string
|
||||
password: string
|
||||
name: string
|
||||
inn: string
|
||||
short_name?: string | null
|
||||
ogrn?: string | null
|
||||
kpp?: string | null
|
||||
legal_address?: string | null
|
||||
actual_address?: string | null
|
||||
bank_details?: BankDetails | null
|
||||
contact_person?: string | null
|
||||
contact_phone?: string | null
|
||||
status?: string
|
||||
}
|
||||
Reference in New Issue
Block a user