feat: добавить новый backend и cabinet
- добавлен новый Nest backend для auth, projects и project access tokens - добавлена control-plane схема БД и миграция Drizzle - перенесён старый backend в old-backend - добавлен React/Vite cabinet с auth-only входом и Mantine layout - обновлены workspace scripts и lockfile
This commit is contained in:
77
apps/cabinet/src/infra/backend-api/client.ts
Normal file
77
apps/cabinet/src/infra/backend-api/client.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { getAdminToken } from './token-storage'
|
||||
import type { AdminSessionResponseDto, LoginRequestDto, LoginResponseDto } from './types/backend-api.type'
|
||||
|
||||
type RequestOptions = {
|
||||
body?: unknown
|
||||
isAuthorized?: boolean
|
||||
method?: 'GET' | 'POST'
|
||||
}
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_API_BASE_URL ?? '/api'
|
||||
|
||||
export const backendApi = {
|
||||
auth: {
|
||||
login: (body: LoginRequestDto) => {
|
||||
return request<LoginResponseDto>('/auth/login', { body, method: 'POST' })
|
||||
},
|
||||
me: () => {
|
||||
return request<AdminSessionResponseDto>('/auth/me', { isAuthorized: true })
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
headers: buildHeaders(options),
|
||||
method: options.method ?? 'GET',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response))
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
function buildHeaders(options: RequestOptions) {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
if (options.isAuthorized) {
|
||||
const token = getAdminToken()
|
||||
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
async function readErrorMessage(response: Response) {
|
||||
try {
|
||||
const value = (await response.json()) as unknown
|
||||
|
||||
if (isRecord(value) && typeof value.message === 'string') {
|
||||
return value.message
|
||||
}
|
||||
|
||||
if (isRecord(value) && Array.isArray(value.message)) {
|
||||
return value.message.join(', ')
|
||||
}
|
||||
} catch {
|
||||
return `request failed with status ${response.status}`
|
||||
}
|
||||
|
||||
return `request failed with status ${response.status}`
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
1
apps/cabinet/src/infra/backend-api/hooks/index.ts
Normal file
1
apps/cabinet/src/infra/backend-api/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { getAdminSessionKey, useGetAdminSession } from './use-get-admin-session.hook'
|
||||
@@ -0,0 +1,18 @@
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
|
||||
import { backendApi } from '../client'
|
||||
import { getAdminToken } from '../token-storage'
|
||||
import type { AdminSessionResponseDto } from '../types/backend-api.type'
|
||||
|
||||
export const getAdminSessionKey = () => ['backend-api', 'auth', 'me'] as const
|
||||
|
||||
/**
|
||||
* Получение текущей admin-сессии.
|
||||
*/
|
||||
export const useGetAdminSession = (config?: SWRConfiguration<AdminSessionResponseDto>) => {
|
||||
const key = getAdminToken() ? getAdminSessionKey() : null
|
||||
const fetcher = () => backendApi.auth.me()
|
||||
|
||||
return useSWR<AdminSessionResponseDto>(key, fetcher, config)
|
||||
}
|
||||
4
apps/cabinet/src/infra/backend-api/index.ts
Normal file
4
apps/cabinet/src/infra/backend-api/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { backendApi } from './client'
|
||||
export { clearAdminToken, getAdminToken, setAdminToken } from './token-storage'
|
||||
export * from './hooks'
|
||||
export type { AdminSessionResponseDto, LoginRequestDto, LoginResponseDto } from './types/backend-api.type'
|
||||
13
apps/cabinet/src/infra/backend-api/token-storage.ts
Normal file
13
apps/cabinet/src/infra/backend-api/token-storage.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const ADMIN_TOKEN_KEY = 'image-platform.cabinet.admin-token'
|
||||
|
||||
export const getAdminToken = () => {
|
||||
return window.localStorage.getItem(ADMIN_TOKEN_KEY)
|
||||
}
|
||||
|
||||
export const setAdminToken = (token: string) => {
|
||||
window.localStorage.setItem(ADMIN_TOKEN_KEY, token)
|
||||
}
|
||||
|
||||
export const clearAdminToken = () => {
|
||||
window.localStorage.removeItem(ADMIN_TOKEN_KEY)
|
||||
}
|
||||
14
apps/cabinet/src/infra/backend-api/types/backend-api.type.ts
Normal file
14
apps/cabinet/src/infra/backend-api/types/backend-api.type.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type LoginRequestDto = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export type LoginResponseDto = {
|
||||
accessToken: string
|
||||
tokenType: 'Bearer'
|
||||
expiresAt: string
|
||||
}
|
||||
|
||||
export type AdminSessionResponseDto = {
|
||||
username: string
|
||||
}
|
||||
2
apps/cabinet/src/infra/theme/index.ts
Normal file
2
apps/cabinet/src/infra/theme/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ThemeProvider } from './theme-provider'
|
||||
export type { ThemeProviderProps } from './types/theme-provider-props.type'
|
||||
22
apps/cabinet/src/infra/theme/theme-provider.tsx
Normal file
22
apps/cabinet/src/infra/theme/theme-provider.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { MantineProvider } from '@mantine/core'
|
||||
import { Notifications } from '@mantine/notifications'
|
||||
|
||||
import type { ThemeProviderProps } from './types/theme-provider-props.type'
|
||||
|
||||
/**
|
||||
* Провайдер визуальной темы кабинета.
|
||||
*
|
||||
* Используется для:
|
||||
* - подключения Mantine theme
|
||||
* - подключения контейнера уведомлений
|
||||
*/
|
||||
export const ThemeProvider = (props: ThemeProviderProps) => {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<MantineProvider defaultColorScheme="light">
|
||||
<Notifications position="top-right" />
|
||||
{children}
|
||||
</MantineProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Пропсы ThemeProvider.
|
||||
*/
|
||||
export type ThemeProviderProps = {
|
||||
/** Контент приложения. */
|
||||
children: ReactNode
|
||||
}
|
||||
Reference in New Issue
Block a user