DZ Hub — устройство проекта¶
Полное описание проекта: из чего он состоит, как запускается, что делает каждый файл конфигурации и каждый слой кода. Состояние на 2026-10-03: фронтенд — ветка
create-auth-flow-and-connect-with-auth-api(коммитe3cc09a), бэкенд — рабочая папка.О продукте (что и зачем делаем) — MVP.md. Здесь — как это сделано.
Содержание¶
- Общая картина
- Быстрый старт
- Структура рабочей папки
- Как фронтенд и бэкенд работают вместе
- Фронтенд
- Бэкенд
- Git и ветки
- Известные недоделки и долги
- Шпаргалка
1. Общая картина¶
DZ Hub — инструмент для парашютного аэродрома: прыжковые дни, взлёты, запись на них,
кабинет парашютиста и панель организатора. Сейчас реализованы регистрация, вход,
выход и текущий пользователь. Остальное (дни, взлёты, допуски) есть только в
мок-прототипе на ветке try/associations.
┌──────────────────────────┐ HTTP + JSON (camelCase) ┌──────────────────────────┐
│ Фронтенд │ Authorization: Bearer JWT │ Бэкенд │
│ React 19 + Vite │ ───────────────────────────► │ FastAPI + SQLAlchemy │
│ http://localhost:5173 │ http://localhost:8000 │ /api/v1/... │
└──────────────────────────┘ /api/v1 └────────────┬─────────────┘
│ asyncpg
┌────────────▼─────────────┐
│ PostgreSQL 18 (Docker) │
│ localhost:5433 │
└──────────────────────────┘
| Часть | Стек | Где | Git |
|---|---|---|---|
| Фронтенд | React 19, TypeScript 6, Vite 8, Tailwind 4, TanStack Query 5 | frontend/ |
свой репозиторий, есть origin |
| Бэкенд | Python 3.13, FastAPI, SQLAlchemy 2 (async), Alembic, uv | backend/ |
пока не git-репозиторий |
| Документы | Markdown | docs/ |
не в git |
Порты, которые нужно помнить:
| Порт | Кто | Почему такой |
|---|---|---|
5173 |
Vite dev-сервер (фронтенд) | жёстко зафиксирован: под него настроен CORS бэкенда |
8000 |
FastAPI (бэкенд) | стандарт uvicorn |
5433 |
PostgreSQL в Docker | не 5432, чтобы не конфликтовать с локальным PostgreSQL |
2. Быстрый старт¶
Нужны: Node.js ≥ 22, Docker (у вас — OrbStack), uv.
# 1. Бэкенд: база + API в Docker, миграции применяются при старте
cd backend
make install # зависимости + .env из .env.example (если .env ещё нет)
make up # http://localhost:8000, документация API: http://localhost:8000/api/docs
# 2. Фронтенд
cd ../frontend
npm install
npm run dev # http://localhost:5173 → «Регистрация» → создать аккаунт
Без бэкенда: в frontend/.env.local положить VITE_ENABLE_MOCKS=true, вход
demo@example.md / password123.
3. Структура рабочей папки¶
DZHub/ ← рабочая папка (открыта в VS Code), сама не git
├── README.md ← краткое описание частей
├── .vscode/ ← настройки VS Code для всей папки (см. 5.13)
├── docs/
│ ├── MVP.md ← о чём договорились по продукту
│ └── PROJECT.md ← этот файл
├── frontend/ ← git-репозиторий фронтенда
└── backend/ ← бэкенд (git ещё не инициализирован)
4. Как фронтенд и бэкенд работают вместе¶
4.1. Адрес бэкенда¶
Фронтенд узнаёт адрес бэкенда так:
VITE_APP_ENV=development ──► src/config/backend.ts ──► env.apiUrl
(из .env) backendUrls.development http://localhost:8000/api/v1
= 'http://localhost:8000'
+ API_PREFIX '/api/v1'
Новое окружение (staging, production) = новая строка в backendUrls. Переменная
VITE_APP_ENV автоматически начнёт принимать новое значение (схема строится из ключей
таблицы). Неизвестное значение → приложение не запустится и покажет ошибку конфигурации.
4.2. CORS¶
Браузер ходит на localhost:8000 напрямую, а страница открыта с localhost:5173 — это
разные «источники» (origin), поэтому браузер требует разрешения от бэкенда (CORS).
- Бэкенд:
CORS_ORIGINS=["http://localhost:5173"]вbackend/.env. Если список пуст, CORS-middleware вообще не подключается (app/main.py). - Фронтенд: в
vite.config.tsстоитstrictPort: true. Если 5173 занят, Vite не запустится, а не уйдёт на 5174, где CORS запросы заблокирует. .envбэкенда читается при старте контейнера. ПоменялиCORS_ORIGINS→docker compose up -d --force-recreate api(илиmake restart).
4.3. Контракт API¶
| Соглашение | Как устроено |
|---|---|
| Формат JSON | camelCase. На бэкенде поля в snake_case, перевод делает CamelModel (alias_generator=to_camel) |
| Префикс | /api/v1 |
| Ошибки | формат FastAPI: {"detail": "текст"} или {"detail": [{loc, msg, type}]} для 422 |
| Авторизация | Authorization: Bearer <JWT> |
| Типы на фронте | пока вручную в frontend/src/shared/api/clients/*.ts, повторяют Pydantic-схемы. План — генерировать из OpenAPI (/api/v1/openapi.json) |
4.4. Эндпоинты, которые существуют сейчас¶
| Метод и путь | Доступ | Ответ | Ошибки |
|---|---|---|---|
GET /api/v1/health |
все | {"status":"ok"} — процесс жив |
— |
GET /api/v1/health/ready |
все | {"status":"ok"} — база доступна |
503 |
POST /api/v1/auth/register |
все | 201 + TokenResponse |
409 email занят, 422 валидация |
POST /api/v1/auth/login |
все | 200 + TokenResponse |
401 неверные данные, 403 аккаунт отключён |
GET /api/v1/auth/me |
с токеном | UserPublic |
401 |
POST /api/v1/auth/logout |
с токеном | 204 |
401 |
Тела запросов и ответов:
// RegisterRequest
{ "email": "a@b.md", "password": "8–128 символов", "firstName": "1–100", "lastName": "1–100" }
// LoginRequest
{ "email": "a@b.md", "password": "..." }
// TokenResponse
{ "accessToken": "eyJ...", "tokenType": "bearer", "user": UserPublic }
// UserPublic
{ "id": "uuid v7", "email": "a@b.md", "firstName": "Ana", "lastName": "Rusu", "fullName": "Ana Rusu" }
4.5. Жизненный цикл сессии (сквозной сценарий)¶
Регистрация/вход
RegisterForm ──► useRegister() ──► authClient.register() ──► POST /auth/register
│ 201 {accessToken, user}
tokenStorage.set(accessToken) ◄─────────────────────────────────┘
queryClient.setQueryData(['auth','session'], user) ← пользователь «вошёл», без лишнего /me
navigate('/')
Перезагрузка страницы
useSession() → токен в localStorage есть? → GET /auth/me → user → «вошёл»
нет → «аноним» → RequireAuth → /login
Токен протух / подделан (любой запрос с токеном вернул 401)
http-client → unauthorizedHandler → tokenStorage.clear() + session = null → /login
Выход
POST /auth/logout (ошибку игнорируем) → удалить токен → очистить весь кэш Query → /login
Важно: токены на сервере не хранятся. «Выход» = фронтенд удаляет свой токен. Сам токен остаётся валидным до истечения срока (7 дней). Отозвать его сейчас нельзя.
5. Фронтенд¶
5.1. Стек и зависимости¶
Рантайм (dependencies, попадают в бандл):
| Пакет | Версия | Зачем |
|---|---|---|
react, react-dom |
19 | UI |
react-router |
8 | маршрутизация (data router: createBrowserRouter) |
@tanstack/react-query |
5 | данные с сервера: кэш, загрузка, повторы, мутации |
react-hook-form + @hookform/resolvers |
7 / 5 | формы |
zod |
4 | схемы валидации: формы и конфиг окружения |
i18next, react-i18next, i18next-browser-languagedetector |
26 / 17 / 8 | переводы RO/RU |
@radix-ui/react-slot, @radix-ui/react-label |
— | примитивы для компонентов в стиле shadcn/ui |
class-variance-authority (cva) |
— | варианты стилей компонентов (variant, size) |
clsx + tailwind-merge |
— | склейка классов без конфликтов (функция cn) |
lucide-react |
1 | иконки |
Разработка (devDependencies):
| Пакет | Зачем |
|---|---|
vite 8, @vitejs/plugin-react |
сборщик и dev-сервер |
typescript ~6.0, @types/* |
типы |
tailwindcss 4 + @tailwindcss/vite |
стили (настраиваются в CSS, без tailwind.config.js) |
vitest 5, jsdom, @testing-library/* |
тесты компонентов |
msw 2 |
моки API для тестов и для режима без бэкенда |
eslint 9 + плагины, typescript-eslint |
линтер |
prettier 3 + prettier-plugin-tailwindcss |
форматирование и сортировка Tailwind-классов |
husky + lint-staged |
проверки перед коммитом |
@tanstack/react-query-devtools |
панель отладки запросов в dev |
engines.node: ">=22" — на старом Node установка предупредит.
5.2. npm-скрипты¶
| Команда | Что делает |
|---|---|
npm run dev |
dev-сервер Vite на http://localhost:5173 (режим development) |
npm run build |
tsc -b (проверка типов) → vite build → папка dist/ |
npm run preview |
раздать собранный dist/ локально |
npm run typecheck |
tsc -b: проверка типов всех проектов TS |
npm run lint / lint:fix |
ESLint / с автоисправлением |
npm run format / format:check |
Prettier: исправить / только проверить |
npm test / test:watch |
Vitest: один прогон / в режиме наблюдения |
npm run check |
typecheck + lint + format:check + test — как в CI |
prepare |
запускается сам после npm install: ставит git-хуки Husky |
5.3. Структура папок¶
frontend/
├── index.html ← HTML-оболочка, <title>%VITE_APP_NAME%</title>, lang="ro"
├── public/
│ ├── favicon.svg
│ └── mockServiceWorker.js ← файл MSW для Service Worker (сейчас не используется, см. 8)
├── src/
│ ├── main.tsx ← точка входа: конфиг → моки → i18n → React
│ ├── index.css ← Tailwind + дизайн-токены
│ ├── vite-env.d.ts ← типы переменных import.meta.env
│ ├── config/
│ │ ├── backend.ts ← таблица «окружение → адрес бэкенда»
│ │ ├── env.ts ← проверка и нормализация всех настроек (zod)
│ │ └── env.test.ts
│ ├── app/ ← СБОРКА приложения
│ │ ├── App.tsx ← AppProviders + RouterProvider
│ │ ├── router.tsx ← все маршруты
│ │ ├── providers/AppProviders.tsx ← QueryClient, devtools, SessionWatcher
│ │ ├── layouts/ ← PublicLayout (гости), AppLayout (вошедшие)
│ │ └── pages/ ← HomePage, NotFoundPage, RouteErrorPage
│ ├── features/ ← БИЗНЕС-МОДУЛИ
│ │ └── auth/
│ │ ├── index.ts ← публичный API модуля
│ │ ├── api/session.ts ← хуки: useSession, useLogin, useRegister, useLogout…
│ │ ├── model/schemas.ts ← zod-схемы форм входа и регистрации
│ │ ├── components/ ← LoginForm, RegisterForm, guards (+ тесты рядом)
│ │ └── pages/ ← LoginPage, RegisterPage
│ ├── shared/ ← ОБЩЕЕ, без бизнес-логики
│ │ ├── api/
│ │ │ ├── http-client.ts ← fetch-обёртка: URL, токен, таймаут, ошибки
│ │ │ ├── api-error.ts ← класс ApiError
│ │ │ ├── token-storage.ts ← где лежит токен (localStorage)
│ │ │ ├── query-client.ts ← настройки TanStack Query
│ │ │ ├── clients/ ← КЛИЕНТЫ БЭКЕНДА, по файлу на модуль API
│ │ │ │ ├── auth.ts ← типы контракта + authClient
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── config/routes.ts ← все URL приложения
│ │ ├── lib/ ← cn(), getErrorMessage()
│ │ └── ui/ ← Button, Card, Input, Label, FormField, Spinner, LanguageSwitcher
│ ├── i18n/
│ │ ├── index.ts ← инициализация i18next
│ │ ├── resources.ts ← реестр namespace'ов
│ │ ├── i18next.d.ts ← типизация ключей
│ │ └── locales/{ro,ru}/{common,auth}.json
│ ├── mocks/ ← фейковый бэкенд (MSW-хендлеры)
│ │ ├── data.ts ← in-memory «база»: демо-пользователь
│ │ ├── handlers/ ← auth.ts, index.ts
│ │ ├── browser.ts ← моки в браузере (подмена fetch)
│ │ ├── server.ts ← моки в тестах (msw/node)
│ │ └── utils.ts ← apiPath()
│ └── test/
│ ├── setup.ts ← глобальная настройка тестов
│ └── render.tsx ← renderWithProviders()
├── Dockerfile, nginx/ ← production-образ (см. 5.12)
├── .github/workflows/ci.yml ← CI
└── конфиги: package.json, tsconfig*.json, vite.config.ts, vitest.config.ts,
eslint.config.js, .prettierrc.json, .editorconfig, .env*, .husky/
5.4. Слои и правила зависимостей¶
app ──────► features ──────► shared
└────────────────────────────▲
config и i18n доступны всем
| Слой | Что можно | Что нельзя |
|---|---|---|
app |
импортировать всё | — |
features/<x> |
shared, config, i18n, другие модули только через @/features/<y> |
лезть внутрь чужого модуля (@/features/y/components/...) |
shared |
config, i18n |
импортировать app и features |
Ещё правило: внутри модуля — относительные пути (../api/session), между слоями —
алиас @/. Всё это проверяет ESLint (см. 5.11), нарушение = ошибка линтера.
Публичный API модуля — его index.ts. Сейчас @/features/auth отдаёт: useSession,
useLogout, useUnauthorizedRedirect, тип Session, RequireAuth,
RedirectIfAuthenticated, тип User, LoginPage, RegisterPage.
5.5. Запуск приложения (main.tsx)¶
Модули грузятся по очереди через динамический import(), и это сделано специально:
import('@/config/env')— проверка конфига. Если он кривой,parseEnvбросаетEnvValidationError, иbootstrap().catchпишет текст ошибки прямо в#rootкрасным. Вместо белого экрана вы видите, какой параметр не так.- Если
VITE_ENABLE_MOCKS=true—startMocks()(подменаfetch). Условиеimport.meta.env.VITE_ENABLE_MOCKS === 'true'Vite вычисляет при сборке, поэтому приfalseкод моков полностью вырезается из бандла. import('@/i18n')— переводы (синхронная инициализация, без «мигания» ключей).- React, ReactDOM и
Appгрузятся параллельно и монтируются в<StrictMode>.
5.6. Конфигурация: .env-файлы и env.ts¶
Порядок загрузки (Vite, каждый следующий перекрывает предыдущий):
.env → .env.[mode] → .env.local → .env.[mode].local.
Режим (mode): development для npm run dev, production для build, test для Vitest.
| Файл | В git | Когда | Содержимое сейчас |
|---|---|---|---|
.env |
да | всегда | значения по умолчанию для всех параметров |
.env.development |
да | npm run dev |
моки выключены, devtools включены, порт 5173 |
.env.test |
да | npm test |
VITE_ENABLE_MOCKS=false (в тестах моки включает setup.ts, а не эта переменная) |
.env.example |
да | никогда | шаблон с описанием каждого параметра |
.env.local |
нет (*.local в .gitignore) |
всегда | ваши личные переопределения |
Все параметры:
| Переменная | По умолчанию | Значения | Что делает |
|---|---|---|---|
VITE_APP_NAME |
"DZ Hub" |
строка | название в шапке и <title> |
VITE_APP_ENV |
development |
ключи backendUrls |
выбирает адрес бэкенда |
VITE_API_TIMEOUT_MS |
15000 |
целое > 0 | таймаут каждого HTTP-запроса |
VITE_ENABLE_MOCKS |
false |
true / false |
отвечать на API-запросы моками вместо бэкенда |
VITE_DEFAULT_LOCALE |
ro |
код языка | язык, если язык браузера не поддерживается; обязан входить в список ниже |
VITE_SUPPORTED_LOCALES |
ro,ru |
через запятую | доступные языки |
VITE_ENABLE_QUERY_DEVTOOLS |
false (true в dev) |
true / false |
панель TanStack Query; работает только в dev-сборке |
DEV_SERVER_PORT |
5173 |
число | порт dev-сервера (без VITE_ → в браузер не попадает) |
⚠️ Всё с префиксом
VITE_вшивается в JavaScript-бандл и видно любому. Никаких секретов в таких переменных.
src/config/env.ts — единственное место, где читается import.meta.env:
- zod-схема проверяет типы, превращает 'true' в true, '15000' в число, 'ro, ru' в
['ro','ru'], проверяет, что язык по умолчанию входит в список;
- наружу отдаёт объект env с удобными полями: appName, appEnv, backendUrl,
apiUrl, apiTimeoutMs, enableMocks, defaultLocale, supportedLocales,
enableQueryDevtools (= DEV && VITE_ENABLE_QUERY_DEVTOOLS), isDev.
Добавить параметр: схема в env.ts + тип в vite-env.d.ts + описание в .env.example
(+ значение в .env).
5.7. HTTP-слой (shared/api)¶
http-client.ts — обёртка над fetch. Каждый запрос:
1. URL = env.apiUrl + путь (+ query-параметры, null/undefined пропускаются);
2. заголовки: Accept: application/json; Content-Type: application/json, если есть тело;
Authorization: Bearer <токен>, если токен есть и не передан auth: false;
3. таймаут через AbortSignal.timeout(env.apiTimeoutMs), объединённый с сигналом отмены
от TanStack Query (AbortSignal.any);
4. ошибки превращаются в ApiError:
| Ситуация | kind |
status |
|---|---|---|
| сервер ответил 4xx/5xx | http |
код ответа; detail → message, массив 422 → issues |
| нет сети / сервер не отвечает / CORS заблокировал | network |
0 |
| превышен таймаут | timeout |
0 |
| отмена запроса (ушли со страницы) | исходная ошибка пробрасывается как есть | — |
- 401 на запросе с токеном → вызывается
unauthorizedHandler(его регистрируетuseUnauthorizedRedirect): токен удаляется, сессия сбрасывается. 204→undefined, иначеresponse.json().
Удобные методы: http.get/post/put/patch/delete.
api-error.ts — класс ApiError с геттерами isUnauthorized (401) и isClientError
(4xx). Функция isApiError(e) — проверка типа.
token-storage.ts — токен в localStorage под ключом dz.accessToken. Все обращения
обёрнуты в try/catch (приватный режим может запрещать хранилище). Вся работа с токеном
только здесь: переход на httpOnly-cookie затронет один файл.
query-client.ts — настройки TanStack Query:
- staleTime: 30_000 — данные свежие 30 секунд, повторный заход на страницу не дёргает сервер;
- refetchOnWindowFocus: false — не перезапрашивать при возврате на вкладку;
- retry: до 2 повторов, но не для 4xx (нет смысла повторять «не найдено»);
- мутации не повторяются.
clients/ — клиенты бэкенда, по файлу на модуль API бэкенда (backend/app/modules/<x>).
Файл содержит типы контракта и объект с функциями запросов. Сейчас:
authClient.register(payload) // POST /auth/register, без токена
authClient.login(payload) // POST /auth/login, без токена
authClient.me(signal) // GET /auth/me
authClient.logout() // POST /auth/logout
Типы User, RegisterRequest, LoginRequest, TokenResponse реэкспортируются из
@/shared/api. Новый модуль бэкенда → clients/<name>.ts + строка в clients/index.ts.
5.8. Модуль auth¶
api/session.ts — всё состояние сессии:
| Хук | Что делает |
|---|---|
useSession() |
запрос ['auth','session'] с staleTime: Infinity. Нет токена → anonymous без запроса. Есть → GET /auth/me; 401 → токен удаляется, anonymous. Возвращает {status: 'loading' \| 'anonymous' \| 'authenticated', user} |
useLogin() |
мутация входа; при успехе сохраняет токен и кладёт user в кэш сессии |
useRegister() |
то же для регистрации (бэкенд сразу отдаёт токен) |
useLogout() |
POST /logout (ошибку игнорирует), удаляет токен, очищает весь кэш запросов |
useUnauthorizedRedirect() |
регистрирует обработчик 401 в http-клиенте; подключён один раз в AppProviders |
Отдельного глобального стора (Redux, Zustand) нет: данные с сервера живут только в TanStack Query, сессия — это тоже просто запрос.
components/guards.tsx:
- RequireAuth — пока сессия грузится, показывает спиннер; аноним → /login, с
state.from = откуда пришёл (после входа вернёт туда же);
- RedirectIfAuthenticated — вошедшего с /login и /register отправляет на /.
Формы (LoginForm, RegisterForm) построены одинаково: react-hook-form + zod-схема из
model/schemas.ts. Тексты ошибок в схеме — ключи переводов ('validation.emailInvalid'),
форма переводит их при выводе.
| Форма | Поля и проверки | Ошибки сервера |
|---|---|---|
| Вход | email (валидный), пароль (не пустой) | 401 → «Неверный email или пароль», 403 → «Аккаунт отключён», сеть/таймаут → понятный текст |
| Регистрация | имя и фамилия (1–100), email, пароль (8–128) — как на бэкенде | 409 → ошибка у поля email, 422 → «Сервер не принял данные», прочее → общий текст |
LoginPage показывает подсказку с демо-аккаунтом только при VITE_ENABLE_MOCKS=true.
5.9. Маршруты и раскладки¶
src/shared/config/routes.ts — все URL: path для роутера, to() для ссылок.
errorElement: RouteErrorPage ← любая ошибка рендера/загрузки, 404 → NotFoundPage
├── RedirectIfAuthenticated
│ └── PublicLayout ← язык справа, логотип и слоган по центру
│ ├── /login LoginPage
│ └── /register RegisterPage
├── RequireAuth
│ └── AppLayout ← шапка: название, имя, язык, выход
│ └── / HomePage ← пока заглушка «Привет, {имя}!»
└── * NotFoundPage
appRoutes экспортируется отдельно от router, чтобы тесты могли собрать
createMemoryRouter из тех же маршрутов.
5.10. Переводы (i18n)¶
- Языки: румынский — эталон, русский. Один JSON на модуль (namespace):
common,auth. - Ключи типизированы (
i18next.d.tsберёт типы из румынских файлов): опечатка в ключеt('login.titel')— ошибка TypeScript. Русский файл типами не проверяется: если ключа нет, покажется румынский текст. - Определение языка: сначала
localStorage['dz.locale'], потом язык браузера. Выбор сохраняется вlocalStorage.<html lang>обновляется при смене языка. - Множественное число — суффиксы i18next:
_one/_few/_other(ro),_one/_few/_many/_other(ru). - Новый модуль:
locales/{ro,ru}/<module>.json+ строка вresources.ts.
5.11. Стили и UI¶
- Tailwind CSS 4: настройка в
src/index.cssчерез@theme, файлаtailwind.config.jsнет. - Дизайн-токены — цвета в
oklch:background,foreground,card,primary,secondary,muted,destructive,success,border,input,ring+ радиусы. Компоненты используют только токены (bg-primary,text-muted-foreground), поэтому бренд и тёмная тема меняются в одном месте. Тема сейчас только светлая. - Шрифт объявлен как
Inter, но сам файл шрифта не подключён — на машинах без Inter используется системный (см. 8). - Компоненты в
shared/uiв стиле shadcn/ui: варианты черезcva, классы черезcn()(clsx+tailwind-merge:cn('px-2', 'px-4')→px-4).ButtonумеетasChild— отдать свои стили дочернему<Link>.FormFieldсвязывает<label>, поле и текст ошибки черезid/aria-describedby.
5.12. Моки (MSW)¶
Одни и те же хендлеры (src/mocks/handlers) работают в двух местах:
| Где | Как подключено | Поведение |
|---|---|---|
| Тесты | src/test/setup.ts → msw/node setupServer |
onUnhandledRequest: 'error': запрос без мока роняет тест |
| Браузер | VITE_ENABLE_MOCKS=true → src/mocks/browser.ts |
подменяет window.fetch; запросы к env.apiUrl отвечают хендлеры, без мока → 501, на бэкенд ничего не уходит; остальные запросы идут как обычно |
Почему в браузере подмена fetch, а не Service Worker (стандартный способ MSW): в Brave
Service Worker не перехватывал запросы, и они молча уходили на бэкенд. Подмена fetch
работает везде. В консоли браузера каждый замоканный запрос пишется строкой [mocks].
data.ts — in-memory «база»: один демо-пользователь demo@example.md / password123.
Хендлеры auth.ts повторяют поведение бэкенда (409 на занятый email, 401 на неверный
пароль, токен вида mock-token-<id>). Данные живут до перезагрузки страницы.
5.13. Тесты¶
- Vitest в окружении
jsdom, конфигvitest.config.tsнаследуетvite.config.ts(алиасы@/работают), тесты —src/**/*.test.{ts,tsx}рядом с кодом. setup.tsперед каждым тестом чиститlocalStorageи ставит румынский язык, после — размонтирует компоненты и сбрасывает переопределения хендлеров.renderWithProviders(ui, { route })— рендер с собственнымQueryClient(без повторов) иMemoryRouter; возвращаетuserиз@testing-library/user-event.- Сейчас 11 тестов:
env.test.ts(4),LoginForm.test.tsx(4),RegisterForm.test.tsx(3). - Подмена ответа в конкретном тесте:
server.use(http.post(apiPath('/auth/login'), () => HttpResponse.error())).
5.14. Качество кода¶
TypeScript (tsconfig.json → два проекта):
- tsconfig.app.json — код в src/: strict, noUncheckedIndexedAccess (элемент массива
может быть undefined), noUnusedLocals/Parameters, erasableSyntaxOnly (запрещены
enum и namespace), verbatimModuleSyntax (типы импортируются через import type),
алиас @/* → src/*;
- tsconfig.node.json — vite.config.ts и vitest.config.ts (окружение Node).
ESLint (eslint.config.js, flat config): рекомендованные правила JS и TypeScript,
React Hooks (включая правила React Compiler: например, запрет Date.now() прямо в рендере),
React Refresh, плюс свои правила:
- consistent-type-imports — типы только через import type;
- неиспользуемые переменные — ошибка (кроме начинающихся с _);
- архитектурные запреты no-restricted-imports из раздела 5.4;
- последним идёт eslint-config-prettier — отключает правила, конфликтующие с Prettier.
Prettier (.prettierrc.json): без точек с запятой, одинарные кавычки, запятые в
конце, ширина 100, плагин сортирует Tailwind-классы (в том числе внутри cn() и cva()).
.prettierignore: dist, coverage, node_modules, mockServiceWorker.js, package-lock.json.
EditorConfig: UTF-8, LF, отступ 2 пробела (4 для .py), перевод строки в конце файла.
Husky + lint-staged: перед каждым коммитом .husky/pre-commit запускает
npx lint-staged — только для изменённых файлов: *.ts,tsx → eslint --fix + prettier;
*.json,css,md,html → prettier. Если ESLint находит неисправимую ошибку, коммит
отменяется.
5.15. CI (.github/workflows/ci.yml)¶
Запускается на push в main и на каждый pull request. Ubuntu, Node 22, кэш npm:
npm ci --ignore-scripts → typecheck → lint → format:check → test → build.
Любой шаг упал — PR красный.
5.16. Docker и nginx¶
Dockerfile — две стадии:
1. build (node:22-alpine): npm ci, затем npm run build. Переменные VITE_*
передаются как --build-arg, потому что вшиваются в бандл при сборке.
2. runtime (nginx:1.29-alpine): только dist/ и конфиг nginx, без Node.
nginx/default.conf.template (переменные подставляет envsubst при старте контейнера):
- /api/ → проксируется на API_UPSTREAM (по умолчанию http://backend:8000);
- /assets/ → кэш на год (immutable): в именах файлов хэш, новая версия = новое имя;
- всё остальное → index.html с no-cache (SPA: маршруты обрабатывает React Router);
- gzip для CSS, JS, JSON, SVG.
⚠️ Пока окружение одно (
development), собранный образ тоже ходит наhttp://localhost:8000, и прокси/api/в nginx не используется. Решить при появлении production (см. 8).
5.17. VS Code¶
Настройки есть в двух местах: DZHub/.vscode/ (когда открыта вся рабочая папка, пути с
префиксом frontend/) и frontend/.vscode/ (когда открыт только фронтенд).
- форматирование при сохранении (Prettier), ESLint-исправления при сохранении;
- TypeScript из node_modules проекта;
- Tailwind IntelliSense знает про index.css и функции cn/cva;
- i18n Ally показывает переводы прямо в коде (эталон — ro).
Рекомендуемые расширения: ESLint, Prettier, Tailwind CSS, i18n Ally, Vitest, EditorConfig.
6. Бэкенд¶
6.1. Стек и зависимости (pyproject.toml)¶
Python 3.13 (.python-version), менеджер — uv (uv.lock фиксирует точные версии).
[tool.uv] package = false — это приложение, а не библиотека.
| Пакет | Зачем |
|---|---|
fastapi |
веб-фреймворк, OpenAPI-документация |
uvicorn[standard] |
ASGI-сервер |
sqlalchemy[asyncio] 2.1 + asyncpg |
ORM и асинхронный драйвер PostgreSQL |
alembic |
миграции схемы базы |
pydantic[email], pydantic-settings |
схемы запросов/ответов, настройки из .env |
pwdlib[argon2] |
хеширование паролей (argon2id) |
pyjwt |
JWT-токены |
dev: pytest, pytest-asyncio, httpx |
тесты |
dev: ruff, mypy |
линтер/форматтер, проверка типов |
6.2. Структура¶
backend/
├── app/
│ ├── main.py ← create_app(): логи, CORS, middleware, роутеры
│ ├── models.py ← реестр моделей для Alembic
│ ├── api/
│ │ ├── router.py ← /api/v1: подключение роутеров модулей
│ │ └── health.py ← /health и /health/ready
│ ├── core/
│ │ ├── config.py ← Settings из .env
│ │ ├── db.py ← движок, сессии, Base, TimestampMixin
│ │ ├── logging.py ← форматы логов, request_id
│ │ ├── middleware.py ← X-Request-ID и лог каждого запроса
│ │ └── schemas.py ← CamelModel
│ └── modules/
│ ├── users/ ← модель User, UserPublic, поиск
│ └── auth/ ← регистрация, вход, токены, CurrentUser
├── alembic/ ← env.py + versions/ (миграции)
├── tests/ ← conftest.py, test_auth.py, test_health.py
├── Dockerfile, docker-compose.yml, Makefile
└── pyproject.toml, uv.lock, alembic.ini, .env, .env.example
Модуль (app/modules/<name>/) всегда устроен одинаково:
| Файл | Что внутри | Правило |
|---|---|---|
models.py |
таблицы SQLAlchemy | импортировать в app/models.py, иначе Alembic не увидит |
schemas.py |
Pydantic-схемы (наследуют CamelModel) |
— |
service.py |
бизнес-логика | не знает про HTTP: ошибки — свои исключения |
router.py |
эндпоинты | проверяет вход, зовёт сервис, переводит исключения в HTTP-коды |
6.3. Настройки (.env → app/core/config.py)¶
Settings (pydantic-settings) читает переменные окружения и файл .env. В коде — только
get_settings() (закэширован через lru_cache).
| Переменная | По умолчанию | Что делает |
|---|---|---|
ENVIRONMENT |
local |
local / test / production; в production проверяется секрет JWT |
LOG_LEVEL |
INFO |
DEBUG / INFO / WARNING / ERROR |
LOG_FORMAT |
text |
text — для глаз, json — для систем сбора логов |
POSTGRES_HOST / PORT |
localhost / 5433 |
адрес базы (в Docker переопределяется на db:5432) |
POSTGRES_USER / PASSWORD / DB |
dzhub ×3 |
учётка и имя базы; те же значения берёт контейнер db |
DB_ECHO |
false |
печатать каждый SQL-запрос |
CORS_ORIGINS |
[] (в вашем .env: ["http://localhost:5173"]) |
JSON-список разрешённых источников |
JWT_SECRET_KEY |
dev-заглушка | секрет подписи токенов; в production обязателен свой, ≥ 32 символа, иначе приложение не запустится |
ACCESS_TOKEN_EXPIRE_MINUTES |
10080 (неделя) |
срок жизни токена |
jwt_algorithm = HS256 и app_name = "DZ Hub API" задаются в коде.
6.4. База данных¶
app/core/db.py:
- engine — асинхронный движок с pool_pre_ping=True (проверяет соединение перед
использованием, переживает перезапуск базы);
- get_session() / SessionDep — одна сессия на запрос. Коммит делает сервис, а не
роутер и не зависимость;
- Base с NAMING_CONVENTION — предсказуемые имена ограничений (pk_users,
uq_users_email, ck_users_email_lowercase…), чтобы Alembic мог найти их и удалить;
- TimestampMixin — created_at / updated_at, значения ставит сама база (now()).
Таблица users (единственная сейчас):
| Колонка | Тип | Особенности |
|---|---|---|
id |
UUID | uuidv7() на стороне базы — требует PostgreSQL 18. UUIDv7 упорядочены по времени, индексу так легче |
email |
varchar(320) | уникальный; CHECK email = lower(email) — хранится только в нижнем регистре |
password_hash |
varchar(255) | argon2id |
first_name, last_name |
varchar(100) | |
is_active |
bool, default true | false → вход даёт 403, токен перестаёт работать |
created_at, updated_at |
timestamptz |
Модель не привязана к аэродрому: аккаунт один на все аэродромы портала. Свойство
full_name вычисляется в Python, в базе его нет.
Миграции (Alembic):
- адрес базы alembic/env.py берёт из get_settings(), а не из alembic.ini;
- compare_type=True — autogenerate замечает смену типов колонок;
- имена файлов: 2026_10_02_2152-<rev>_<slug>.py; после генерации файл автоматически
прогоняется через ruff --fix и ruff format (post_write_hooks);
- сейчас одна миграция: b81f8147cc9f_create_users;
- порядок работы: поменять модель → make makemigration m="..." → прочитать
сгенерированный файл → make migrate.
6.5. Авторизация (modules/auth)¶
Пароли (security.py): PasswordHash.recommended() = argon2id. Если пользователь с
таким email не найден, всё равно проверяется «пустой» хеш (_DUMMY_HASH): время ответа
одинаковое, и по нему нельзя понять, зарегистрирован ли email.
Токены: JWT HS256 с полями sub (id пользователя), iat, exp. При проверке sub и
exp обязательны. Подделанный, просроченный или битый токен → None → 401.
CurrentUser (dependencies.py) — зависимость для защищённых эндпоинтов:
нет заголовка / плохой токен / пользователь удалён или отключён → 401 с
WWW-Authenticate: Bearer. Использование: параметр user: CurrentUser.
Сервис (service.py):
- register: email нормализуется (strip().lower()), проверяется занятость, пароль
хешируется, коммит. Если параллельный запрос успел занять тот же email, ловится
IntegrityError → тоже EmailAlreadyRegisteredError → 409;
- authenticate: неверный email и неверный пароль дают одинаковый ответ 401;
отключённый пользователь → 403.
6.6. Логи и request_id¶
- Каждый запрос получает request_id: берётся из заголовка
X-Request-IDили генерируется (12 hex-символов), возвращается в ответе и попадает в каждую строку лога этого запроса (черезcontextvars). Ошибку из браузера можно найти в логах по этому id. - Формат
text:08:15:02 INFO app.request [a1b2c3d4e5f6] POST /api/v1/auth/login 200 duration_ms=12.3. Форматjson: одна JSON-строка на запись, для Loki/ELK. - Логи uvicorn направлены в общий обработчик; его access-лог отключён: запросы пишет своя middleware.
- Уровень строки о запросе: 5xx →
WARNING; успешные/health→DEBUG(healthcheck не засоряет лог); остальное →INFO. Необработанное исключение →exceptionсо стеком. - Свои поля:
logger.info("Взлёт создан", extra={"load_id": load.id}).
6.7. Здоровье сервиса¶
GET /api/v1/health— процесс жив (база не проверяется). Его дёргаетHEALTHCHECKв Dockerfile.GET /api/v1/health/ready—SELECT 1в базу; недоступна →503.
6.8. Тесты¶
pytest+pytest-asyncio(asyncio_mode = auto, один event loop на сессию).- Отдельная база
dzhub_test: создаётся автоматически, схема пересоздаётся при каждом запуске (drop_all+create_all, не через миграции). - Каждый тест в транзакции, которая откатывается:
commit()в коде приложения превращается в SAVEPOINT, тесты не видят данных друг друга, рабочая база не затрагивается. - HTTP-клиент —
httpx.AsyncClientсASGITransport(без настоящего сервера), сессия подменяется черезapp.dependency_overrides. - 16 тестов: health (4) и auth (12): регистрация, регистронезависимый дубль email,
валидация, хеширование пароля, вход, неверный пароль, неизвестный email выглядит как
неверный пароль, отключённый пользователь,
/meс токеном/без/с битым, выход. - Нужна запущенная база:
make db.
6.9. Docker, Compose, Makefile¶
docker-compose.yml (проект dzhub):
- db — postgres:18-alpine, порт 5433→5432, данные в томе pgdata, healthcheck через
pg_isready;
- api — собирается из Dockerfile, читает .env, но POSTGRES_HOST=db,
POSTGRES_PORT=5432; стартует только после того, как db здоров. Команда:
alembic upgrade head && uvicorn ... --reload. Папки app/ и alembic/ смонтированы
внутрь, поэтому правки кода подхватываются без пересборки.
Dockerfile — две стадии: builder ставит зависимости через uv (с кэшем, без
dev-зависимостей, сначала только lock-файл — для кэша слоёв), финальный образ
python:3.13-slim запускается не от root (пользователь app), с HEALTHCHECK.
Makefile (make help — список):
| Команда | Что делает |
|---|---|
make install |
uv sync + .env из .env.example, если его нет |
make dev |
API локально с автоперезагрузкой (база — через make db) |
make db |
только PostgreSQL в Docker |
make up / down / restart |
база + API в Docker / остановить / перезапустить |
make logs, ps, psql |
логи, статус контейнеров, консоль PostgreSQL |
make migrate / downgrade / history |
применить / откатить последнюю / история миграций |
make makemigration m="..." |
сгенерировать миграцию по изменениям моделей |
make test |
pytest |
make lint / format / typecheck |
ruff (проверка) / ruff (исправление) / mypy |
make check |
lint + typecheck + test |
make clean |
⚠️ удалить контейнеры и данные базы |
6.10. Качество кода¶
- ruff: ширина 100, Python 3.13, правила E, W, F, I (порядок импортов), B, UP, SIM, ASYNC, RUF; отключены RUF001–003 (иначе ругается на кириллицу в строках и комментариях).
- mypy:
strict = true+ плагин pydantic; папка миграций исключена.
7. Git и ветки¶
Фронтенд (frontend/, remote origin):
| Ветка | Что там |
|---|---|
main |
каркас + «changed app to dz hub» (5526efe, перенесён cherry-pick'ом) |
create-auth-flow-and-connect-with-auth-api |
текущая: main + подключение настоящего API (e3cc09a) |
try/associations |
мок-прототип всего MVP: дни, взлёты, запись, допуски, самолёты, кабинет. Только на моках, роли пользователя (организатор/парашютист) на бэкенде нет |
Коммит «changed app to dz hub» существует дважды с разными хэшами (9d454a0 в
try/associations и 5526efe в main) — следствие cherry-pick. При слиянии git обычно
разберётся сам; rebase try/associations на main уберёт дубль.
Бэкенд в git ещё не добавлен: правки в backend/ (например, CORS) нигде не сохранены,
кроме диска.
8. Известные недоделки и долги¶
| Что | Почему важно | Что сделать |
|---|---|---|
| Бэкенд не в git | нет истории и резервной копии | git init, первый коммит, remote |
| Нет production-окружения | Docker-образ фронтенда ходит на localhost:8000 |
добавить production в backendUrls (вероятно, относительный '' → запросы на тот же домен через nginx /api/) и --build-arg VITE_APP_ENV=production |
Токен в localStorage, не отзывается |
при XSS токен можно украсть; после выхода он валиден ещё до 7 дней | позже: httpOnly-cookie (меняется только token-storage.ts + бэкенд) и/или чёрный список токенов |
| Типы API пишутся вручную | могут разъехаться с бэкендом | генерация из /api/v1/openapi.json (например, openapi-typescript) |
public/mockServiceWorker.js и "msw.workerDirectory" в package.json |
остались от Service Worker-моков, сейчас не используются | можно удалить |
Шрифт Inter не подключён |
выглядит по-разному на разных машинах | подключить файл шрифта или убрать из --font-sans |
README.md фронтенда: образ ham-frontend в разделе Docker |
имя от другого проекта | переименовать в dz-hub-frontend |
Корневой README.md: бэкенд «не начат» |
устарело | обновить таблицу |
| Нет ролей и допусков на бэкенде | прототип try/associations на них опирается |
следующие модули бэкенда по MVP.md |
Тестовая база создаётся через create_all, не через миграции |
ошибку в миграции тесты не поймают | позже прогонять alembic upgrade head в тестах или в CI |
| Нет CI для бэкенда | make check никто не запускает автоматически |
GitHub Actions с сервисом PostgreSQL |
HomePage — заглушка |
— | заменить первым настоящим экраном |
9. Шпаргалка¶
Добавить модуль от начала до конца (на примере «прыжковые дни»):
- Бэкенд:
app/modules/jump_days/{models,schemas,service,router}.py→ импорт модели вapp/models.py→ роутер вapp/api/router.py→make makemigration m="add jump days"→ проверить файл →make migrate→ тесты вtests/. - Фронтенд, клиент:
src/shared/api/clients/jump-days.ts(типы + запросы) → экспорт вclients/index.ts. - Фронтенд, модуль:
src/features/jump-days/{api,model,components,pages}+index.ts. - Маршрут: URL в
shared/config/routes.ts, страница вapp/router.tsx. - Переводы:
i18n/locales/{ro,ru}/jumpDays.json+i18n/resources.ts. - Моки:
mocks/handlers/jump-days.ts+handlers/index.ts(нужны для тестов).
Частые ситуации:
| Симптом | Причина и решение |
|---|---|
| На экране красный текст «Некорректная конфигурация окружения» | проверьте .env* и .env.local: какое поле названо в ошибке |
| «Нет связи с сервером» на фронте | бэкенд не запущен (make up) или CORS: проверьте CORS_ORIGINS и что фронт на 5173 |
| Vite не стартует: порт занят | закрыть другой npm run dev (порт фиксирован намеренно) |
Поменяли backend/.env, а эффекта нет |
docker compose up -d --force-recreate api |
| Хочу работать без бэкенда | VITE_ENABLE_MOCKS=true в frontend/.env.local, вход demo@example.md / password123 |
| Коммит отменился сам | lint-staged нашёл ошибку ESLint: исправить и закоммитить снова |
| Ищу ошибку запроса в логах бэкенда | взять X-Request-ID из ответа (DevTools → Network) и найти его в make logs |
| Документация API | http://localhost:8000/api/docs |