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:
2026-05-12 09:22:04 +03:00
parent d49449c30c
commit 98295d0569
113 changed files with 3426 additions and 169 deletions

View 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)
}

View File

@@ -0,0 +1 @@
export { getAdminSessionKey, useGetAdminSession } from './use-get-admin-session.hook'

View File

@@ -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)
}

View 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'

View 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)
}

View 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
}

View File

@@ -0,0 +1,2 @@
export { ThemeProvider } from './theme-provider'
export type { ThemeProviderProps } from './types/theme-provider-props.type'

View 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>
)
}

View File

@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'
/**
* Пропсы ThemeProvider.
*/
export type ThemeProviderProps = {
/** Контент приложения. */
children: ReactNode
}