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

12
apps/cabinet/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Assets Cabinet</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script>
</body>
</html>

36
apps/cabinet/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "@image-platform/cabinet",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -b && vite build",
"codegen:backend-api": "npx @gromlab/api-codegen@latest -i http://localhost:3001/docs-json -o src/infra/backend-api/generated -n backend-api.generated",
"dev": "vite --host 0.0.0.0 --port 5174",
"preview": "vite preview --host 0.0.0.0 --port 5174",
"typecheck": "tsc -b"
},
"dependencies": {
"@mantine/core": "^9.1.1",
"@mantine/form": "^9.1.1",
"@mantine/hooks": "^9.1.1",
"@mantine/notifications": "^9.1.1",
"clsx": "^2.1.1",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.15.0",
"swr": "^2.4.1"
},
"devDependencies": {
"@csstools/postcss-global-data": "^4.0.0",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"postcss-custom-media": "^12.0.1",
"postcss-nesting": "^14.0.0",
"typescript": "^5.9.3",
"vite": "^8.0.10"
}
}

View File

@@ -0,0 +1,10 @@
export default {
plugins: {
'@csstools/postcss-global-data': {
files: ['src/shared/styles/media.css'],
},
'postcss-custom-media': {},
'postcss-nesting': {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,23 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { LoginScreen } from 'screens/login'
import { SessionScreen } from 'screens/session'
import { AuthenticatedShell } from './authenticated-shell'
import { ProtectedRoute } from './protected-route'
export const AppRouter = () => (
<Routes>
<Route element={<LoginScreen />} path="/login" />
<Route
element={
<ProtectedRoute>
<AuthenticatedShell>
<SessionScreen />
</AuthenticatedShell>
</ProtectedRoute>
}
path="/"
/>
<Route element={<Navigate replace to="/" />} path="*" />
</Routes>
)

View File

@@ -0,0 +1,17 @@
import { BrowserRouter } from 'react-router-dom'
import { SWRConfig } from 'swr'
import { ThemeProvider } from 'infra/theme'
import { AppRouter } from './app-router'
export const App = () => {
return (
<BrowserRouter>
<SWRConfig value={{ shouldRetryOnError: false }}>
<ThemeProvider>
<AppRouter />
</ThemeProvider>
</SWRConfig>
</BrowserRouter>
)
}

View File

@@ -0,0 +1,40 @@
import { Loader, Center } from '@mantine/core'
import { useNavigate } from 'react-router-dom'
import { authFactory } from 'business/auth'
import { MainLayout } from 'layouts/main'
import type { AuthenticatedShellProps } from './types/authenticated-shell-props.type'
const auth = authFactory()
/**
* Обёртка защищённого layout кабинета.
*
* Используется для:
* - передачи admin-сессии в MainLayout
* - выполнения выхода из кабинета
*/
export const AuthenticatedShell = (props: AuthenticatedShellProps) => {
const { children } = props
const navigate = useNavigate()
const session = auth.useAdminSession()
const logoutAction = auth.useLogout()
const handleLogout = () => {
void logoutAction.logout().then(() => navigate('/login', { replace: true }))
}
if (!session.session) {
return (
<Center h="100vh">
<Loader />
</Center>
)
}
return (
<MainLayout username={session.session.username} onLogout={handleLogout}>
{children}
</MainLayout>
)
}

View File

@@ -0,0 +1,18 @@
import 'shared/styles/global.css'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './app'
const root = document.getElementById('root')
if (!root) {
throw new Error('Root element #root not found')
}
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,33 @@
import { Loader, Center } from '@mantine/core'
import { Navigate } from 'react-router-dom'
import { authFactory } from 'business/auth'
import type { ProtectedRouteProps } from './types/protected-route-props.type'
const auth = authFactory()
/**
* Guard защищённых маршрутов кабинета.
*
* Используется для:
* - проверки admin-сессии через SWR
* - перенаправления неавторизованного пользователя на вход
*/
export const ProtectedRoute = (props: ProtectedRouteProps) => {
const { children } = props
const session = auth.useAdminSession()
if (session.isLoading) {
return (
<Center h="100vh">
<Loader />
</Center>
)
}
if (!session.isAuthenticated) {
return <Navigate replace to="/login" />
}
return children
}

View File

@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'
/**
* Пропсы AuthenticatedShell.
*/
export type AuthenticatedShellProps = {
/** Контент защищённого маршрута. */
children: ReactNode
}

View File

@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'
/**
* Пропсы ProtectedRoute.
*/
export type ProtectedRouteProps = {
/** Защищённый route element. */
children: ReactNode
}

View File

@@ -0,0 +1,15 @@
import { useAdminSession } from './hooks/use-admin-session.hook'
import { useLogin } from './hooks/use-login.hook'
import { useLogout } from './hooks/use-logout.hook'
import type { AuthFactory } from './types/auth-factory.type'
/**
* Создаёт runtime API бизнес-модуля Auth.
*/
export const authFactory: AuthFactory = () => {
return {
useAdminSession,
useLogin,
useLogout,
}
}

View File

@@ -0,0 +1,20 @@
import { getAdminToken, useGetAdminSession } from 'infra/backend-api'
import type { AdminSession } from '../types/auth-api.type'
/**
* Состояние текущей admin-сессии.
*/
export const useAdminSession = (): AdminSession => {
const hasToken = Boolean(getAdminToken())
const sessionQuery = useGetAdminSession({
shouldRetryOnError: false,
})
return {
error: sessionQuery.error,
isAuthenticated: Boolean(sessionQuery.data),
isLoading: hasToken && sessionQuery.isLoading,
session: sessionQuery.data ?? null,
}
}

View File

@@ -0,0 +1,40 @@
import { useState } from 'react'
import { useSWRConfig } from 'swr'
import { backendApi, getAdminSessionKey, setAdminToken } from 'infra/backend-api'
import { toError } from '../lib/to-error'
import type { LoginAction, LoginInput } from '../types/auth-api.type'
/**
* Сценарий входа администратора.
*/
export const useLogin = (): LoginAction => {
const { mutate } = useSWRConfig()
const [error, setError] = useState<Error | null>(null)
const [isLoggingIn, setIsLoggingIn] = useState(false)
const login = async (input: LoginInput) => {
setError(null)
setIsLoggingIn(true)
try {
const session = await backendApi.auth.login(input)
setAdminToken(session.accessToken)
await mutate(getAdminSessionKey())
return session
} catch (caughtError) {
const nextError = toError(caughtError)
setError(nextError)
throw nextError
} finally {
setIsLoggingIn(false)
}
}
return {
error,
isLoggingIn,
login,
}
}

View File

@@ -0,0 +1,20 @@
import { useSWRConfig } from 'swr'
import { clearAdminToken, getAdminSessionKey } from 'infra/backend-api'
import type { LogoutAction } from '../types/auth-api.type'
/**
* Сценарий выхода администратора.
*/
export const useLogout = (): LogoutAction => {
const { mutate } = useSWRConfig()
const logout = async () => {
clearAdminToken()
await mutate(getAdminSessionKey(), undefined, { revalidate: false })
}
return {
logout,
}
}

View File

@@ -0,0 +1,3 @@
export { authFactory } from './auth.factory'
export type { AdminSession, AuthApi, LoginAction, LoginInput, LogoutAction } from './types/auth-api.type'
export type { AuthFactory } from './types/auth-factory.type'

View File

@@ -0,0 +1,7 @@
export const toError = (value: unknown) => {
if (value instanceof Error) {
return value
}
return new Error(String(value))
}

View File

@@ -0,0 +1,29 @@
import type { AdminSessionResponseDto, LoginRequestDto, LoginResponseDto } from 'infra/backend-api'
export type LoginInput = LoginRequestDto
export type AdminSession = {
error?: Error
isAuthenticated: boolean
isLoading: boolean
session: AdminSessionResponseDto | null
}
export type LoginAction = {
error: Error | null
isLoggingIn: boolean
login: (input: LoginInput) => Promise<LoginResponseDto>
}
export type LogoutAction = {
logout: () => Promise<void>
}
/**
* Публичный runtime API бизнес-модуля Auth.
*/
export type AuthApi = {
useAdminSession: () => AdminSession
useLogin: () => LoginAction
useLogout: () => LogoutAction
}

View File

@@ -0,0 +1,3 @@
import type { AuthApi } from './auth-api.type'
export type AuthFactory = () => AuthApi

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
}

View File

@@ -0,0 +1,2 @@
export { MainLayout } from './main.layout'
export type { MainLayoutProps } from './types/main-layout-props.type'

View File

@@ -0,0 +1,82 @@
import { AppShell, Burger, Button, Container, Group, NavLink, Stack, Text, ThemeIcon } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import cl from 'clsx'
import styles from './styles/main-layout.module.css'
import type { MainLayoutProps } from './types/main-layout-props.type'
/**
* Основной layout кабинета с Mantine AppShell.
*
* Используется для:
* - оборачивания защищённых маршрутов кабинета
* - отображения шапки, сайдбара и футера
*/
export const MainLayout = (props: MainLayoutProps) => {
const { children, className, onLogout, username, ...rootAttrs } = props
const [isNavbarOpened, navbar] = useDisclosure(false)
return (
<AppShell
{...rootAttrs}
className={cl(styles.root, className)}
footer={{ height: 48 }}
header={{ height: 72 }}
navbar={{ breakpoint: 'md', collapsed: { mobile: !isNavbarOpened }, width: 280 }}
padding="md"
>
<AppShell.Header>
<Group h="100%" justify="space-between" px={{ base: 'md', md: 'xl' }}>
<Group gap="md">
<Burger hiddenFrom="md" opened={isNavbarOpened} size="sm" onClick={navbar.toggle} />
<Group gap="sm" aria-label="Assets Delivery Platform">
<ThemeIcon radius="xl" size={42} variant="light">
AD
</ThemeIcon>
<Text fw={700} visibleFrom="sm">
Assets Delivery
</Text>
</Group>
</Group>
<Group gap="sm">
<Text c="dimmed" size="sm">
{username}
</Text>
<Button color="gray" variant="light" onClick={onLogout}>
Выйти
</Button>
</Group>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<Stack gap="sm">
<Text c="dimmed" fw={700} size="xs" tt="uppercase">
Навигация
</Text>
<NavLink active label="Вход выполнен" />
<NavLink disabled label="Проекты появятся позже" />
</Stack>
</AppShell.Navbar>
<AppShell.Main>
<Container py="xl" size="lg">
{children}
</Container>
</AppShell.Main>
<AppShell.Footer>
<Group h="100%" justify="space-between" px={{ base: 'md', md: 'xl' }}>
<Text c="dimmed" size="sm">
Control plane
</Text>
<Text c="dimmed" size="sm">
REST + SWR
</Text>
</Group>
</AppShell.Footer>
</AppShell>
)
}

View File

@@ -0,0 +1,3 @@
.root {
min-height: 100vh;
}

View File

@@ -0,0 +1,18 @@
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
/**
* Параметры MainLayout.
*/
export type MainLayoutParams = {
/** Контент текущего маршрута. */
children: ReactNode
/** Имя авторизованного администратора. */
username: string
/** Обработчик выхода из кабинета. */
onLogout: () => void
}
/** Атрибуты корневого элемента без children. */
type RootAttrs = Omit<ComponentPropsWithoutRef<'div'>, 'children'>
export type MainLayoutProps = RootAttrs & MainLayoutParams

View File

@@ -0,0 +1,2 @@
export { LoginScreen } from './login.screen'
export type { LoginScreenProps } from './types/login-screen-props.type'

View File

@@ -0,0 +1,106 @@
import {
Alert,
Box,
Button,
Center,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
ThemeIcon,
Title,
} from '@mantine/core'
import { useForm } from '@mantine/form'
import { notifications } from '@mantine/notifications'
import cl from 'clsx'
import { Navigate, useNavigate } from 'react-router-dom'
import { authFactory } from 'business/auth'
import styles from './styles/login.module.css'
import type { LoginScreenProps } from './types/login-screen-props.type'
const auth = authFactory()
/**
* Экран входа администратора.
*
* Используется для:
* - получения admin Bearer token
* - входа в защищенный layout кабинета
*/
export const LoginScreen = (props: LoginScreenProps) => {
const { className, ...rootAttrs } = props
const navigate = useNavigate()
const session = auth.useAdminSession()
const loginAction = auth.useLogin()
const form = useForm({
initialValues: {
password: 'admin',
username: 'admin',
},
})
if (session.isAuthenticated) {
return <Navigate replace to="/" />
}
const handleSubmit = form.onSubmit(async (values) => {
try {
await loginAction.login(values)
notifications.show({
color: 'green',
message: 'Admin-сессия активна.',
title: 'Вход выполнен',
})
navigate('/', { replace: true })
} catch (error) {
notifications.show({
color: 'red',
message: error instanceof Error ? error.message : 'Не удалось войти.',
title: 'Ошибка входа',
})
}
})
return (
<section {...rootAttrs} className={cl(styles.root, className)}>
<Center mih="100vh" p="md">
<Box w="100%" maw={460}>
<Stack gap="lg">
<Stack align="center" gap="xs">
<ThemeIcon radius="xl" size={52} variant="light">
AD
</ThemeIcon>
<Title order={1} ta="center">
Assets Delivery
</Title>
<Text c="dimmed" ta="center">
Вход в кабинет управления платформой.
</Text>
</Stack>
<Paper p="xl" radius="lg" shadow="sm" withBorder>
<form onSubmit={handleSubmit}>
<Stack gap="md">
{loginAction.error ? (
<Alert color="red" radius="lg" title="Вход не выполнен">
{loginAction.error.message}
</Alert>
) : null}
<TextInput label="Логин" placeholder="admin" required {...form.getInputProps('username')} />
<PasswordInput label="Пароль" placeholder="admin" required {...form.getInputProps('password')} />
<Button loading={loginAction.isLoggingIn} type="submit">
Войти
</Button>
</Stack>
</form>
</Paper>
</Stack>
</Box>
</Center>
</section>
)
}

View File

@@ -0,0 +1,3 @@
.root {
min-height: 100vh;
}

View File

@@ -0,0 +1,11 @@
import type { ComponentPropsWithoutRef } from 'react'
/**
* Параметры LoginScreen.
*/
export type LoginScreenParams = Record<string, never>
/** Атрибуты корневого элемента без children. */
type RootAttrs = Omit<ComponentPropsWithoutRef<'section'>, 'children'>
export type LoginScreenProps = RootAttrs & LoginScreenParams

View File

@@ -0,0 +1,2 @@
export { SessionScreen } from './session.screen'
export type { SessionScreenProps } from './types/session-screen-props.type'

View File

@@ -0,0 +1,52 @@
import { Badge, Group, Paper, Stack, Text, ThemeIcon, Title } from '@mantine/core'
import cl from 'clsx'
import { authFactory } from 'business/auth'
import styles from './styles/session.module.css'
import type { SessionScreenProps } from './types/session-screen-props.type'
const auth = authFactory()
/**
* Экран активной admin-сессии.
*
* Используется для:
* - подтверждения успешного входа
* - отображения базового состояния кабинета до продуктовых экранов
*/
export const SessionScreen = (props: SessionScreenProps) => {
const { className, ...rootAttrs } = props
const session = auth.useAdminSession()
return (
<section {...rootAttrs} className={cl(styles.root, className)}>
<Paper p="xl" radius="lg" shadow="sm" withBorder>
<Stack gap="lg">
<Group justify="space-between">
<Group gap="md">
<ThemeIcon color="green" radius="xl" size={48} variant="light">
OK
</ThemeIcon>
<div>
<Title order={1}>Вход выполнен</Title>
<Text c="dimmed">Admin-сессия получена через новый backend.</Text>
</div>
</Group>
<Badge color="green" variant="light">
active
</Badge>
</Group>
<Text>
Пользователь: <strong>{session.session?.username ?? 'admin'}</strong>
</Text>
<Text c="dimmed">
Продуктовые экраны не подключены. Следующим шагом сюда можно добавить проекты и токены доступа к проектам.
</Text>
</Stack>
</Paper>
</section>
)
}

View File

@@ -0,0 +1,3 @@
.root {
display: block;
}

View File

@@ -0,0 +1,11 @@
import type { ComponentPropsWithoutRef } from 'react'
/**
* Параметры SessionScreen.
*/
export type SessionScreenParams = Record<string, never>
/** Атрибуты корневого элемента без children. */
type RootAttrs = Omit<ComponentPropsWithoutRef<'section'>, 'children'>
export type SessionScreenProps = RootAttrs & SessionScreenParams

View File

@@ -0,0 +1,3 @@
@import "@mantine/core/styles.css";
@import "@mantine/notifications/styles.css";
@import "./variables.css";

View File

@@ -0,0 +1,17 @@
/* Ширина - Mobile First (min-width), кроме --xs (max-width). */
@custom-media --xs (max-width: 35.9375rem);
@custom-media --sm (min-width: 36rem);
@custom-media --md (min-width: 48rem);
@custom-media --lg (min-width: 62rem);
@custom-media --xl (min-width: 75rem);
@custom-media --2xl (min-width: 88rem);
@custom-media --3xl (min-width: 120rem);
/* Высота - min-height. */
@custom-media --h-xs (min-height: 41.6875rem);
@custom-media --h-sm (min-height: 43.875rem);
@custom-media --h-md (min-height: 50.625rem);
@custom-media --h-lg (min-height: 56.25rem);
@custom-media --h-xl (min-height: 62.5rem);
@custom-media --h-2xl (min-height: 68.75rem);
@custom-media --h-3xl (min-height: 75rem);

View File

@@ -0,0 +1,12 @@
:root {
--color-primary: #228be6;
--color-bg: #ffffff;
--color-bg-hover: #f8f9fa;
--color-text: #1a1b1e;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--radius-1: 4px;
--radius-2: 8px;
}

1
apps/cabinet/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"app/*": ["src/app/*"],
"layouts/*": ["src/layouts/*"],
"screens/*": ["src/screens/*"],
"widgets/*": ["src/widgets/*"],
"business/*": ["src/business/*"],
"infra/*": ["src/infra/*"],
"ui/*": ["src/ui/*"],
"shared/*": ["src/shared/*"]
}
},
"include": ["src"]
}

View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,31 @@
import { fileURLToPath, URL } from 'node:url'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
const srcPath = (path: string) => fileURLToPath(new URL(`./src/${path}`, import.meta.url))
const backendProxyTarget = process.env.CABINET_BACKEND_PROXY_TARGET ?? 'http://localhost:3001'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
app: srcPath('app'),
layouts: srcPath('layouts'),
screens: srcPath('screens'),
widgets: srcPath('widgets'),
business: srcPath('business'),
infra: srcPath('infra'),
ui: srcPath('ui'),
shared: srcPath('shared'),
},
},
server: {
proxy: {
'/api': {
changeOrigin: true,
target: backendProxyTarget,
},
},
},
})