Frontend-приложения Админки ГК и Личного кабинета франчайзи¶
Статус документа¶
Документ фиксирует выбранную архитектуру двух административных frontend-приложений и их контракты с монолитом. Приложения создаются и развиваются независимо; их реализация находится за пределами этого репозитория.
Приложения и границы¶
Админка ГК и Личный кабинет франчайзи являются двумя самостоятельными продуктами:
| Контур | Репозиторий | Production origin | API | JWT audience |
|---|---|---|---|---|
| Админка ГК | git@gitlab.tapir.ws:panam/backend/v2/admin.git |
https://admin.panampizza.ru |
/api/admin/v1/** |
admin |
| Личный кабинет ФЧ | git@gitlab.tapir.ws:panam/backend/v2/franchise-lk.git |
https://franchise.panampizza.ru |
/api/franchise/v1/** |
franchise-office |
Для каждого приложения используются отдельные репозиторий, сборка, CI/CD и жизненный цикл релизов. Frontend-код не включается в Gradle-модули монолита.
Приложения не объединяются в frontend-монорепозиторий: у них разные пользователи, JWT audience, permissions, сценарии и темп развития, а устойчивого общего слоя почти нет. Одинаковые на первый взгляд компоненты сначала реализуются независимо. Общий package допускается только после появления стабильного фактического дублирования и отдельного решения о его владении и версионировании.
Технологический стек¶
Оба frontend-приложения являются SPA без SSR. Базовый стек:
- React и TypeScript;
- Vite для разработки и сборки;
- Ant Design как библиотека интерфейсных компонентов;
- React Router для маршрутизации;
- TanStack Query для серверного состояния;
- React Hook Form для состояния и валидации форм;
- генерируемый TypeScript-клиент из OpenAPI монолита;
- Vitest для модульных и компонентных тестов;
- Playwright для критических пользовательских сценариев.
Refine и готовый административный шаблон не используются: значительная часть интерфейса будет предметной и нестандартной. Отдельный глобальный state manager не вводится заранее. Серверными данными владеет TanStack Query, формами — React Hook Form, а небольшим состоянием текущей сессии — auth context приложения.
Развёртывание и сетевой контур¶
Статические файлы SPA и API соответствующего контура доступны браузеру через один origin. Edge
proxy отдаёт frontend и направляет /api/** в монолит:
https://admin.panampizza.ru/ → SPA Админки ГК
https://admin.panampizza.ru/api/admin/** → monolith /api/admin/**
https://franchise.panampizza.ru/ → SPA Личного кабинета ФЧ
https://franchise.panampizza.ru/api/franchise/** → monolith /api/franchise/**
Frontend вызывает API относительными URL. Это исключает штатную зависимость от CORS и сохраняет
cookie в origin соответствующего приложения. SPA fallback применяется только к frontend-маршрутам
и не должен превращать неизвестный /api/** в index.html.
Для локальной разработки Vite dev server проксирует API-пространство приложения в локально запущенный монолит.
Сессия и токены¶
Access token возвращается auth-endpoint в JSON и хранится только в памяти приложения. Он не
записывается в localStorage, sessionStorage, IndexedDB или доступную JavaScript cookie. Каждый
прикладной запрос передаёт его в Authorization: Bearer.
Refresh token хранится только в установленной backend HttpOnly cookie соответствующего контура. Frontend не читает и не переносит его самостоятельно. После перезагрузки страницы приложение восстанавливает сессию вызовом refresh endpoint, получает новый access token и только после этого строит защищённый интерфейс.
Ответ 401 обрабатывается централизованно. Допускается одна согласованная попытка refresh и
повторение исходного запроса; параллельные 401 не должны запускать несколько ротаций одного
refresh token. Неуспешный refresh очищает frontend-сессию и переводит пользователя на страницу
входа. 403 не запускает refresh и отображается как отсутствие разрешения на операцию.
Вкладки одного origin разделяют refresh-cookie, но держат access token в собственной памяти.
Frontend координирует refresh через navigator.locks и передаёт актуальный access token через
BroadcastChannel; при отсутствии другой вкладки новая вкладка вызывает refresh сама. Это
оптимизация: backend сохраняет стабильный sid login-session и идемпотентно возвращает один
successor при параллельной ротации. Разные устройства получают независимые login-session; logout
завершает только текущее устройство, а смена пароля или блокировка доступа завершает все.
Обязательная смена временного пароля является отдельным состоянием сессии. Пока
passwordChangeRequired=true, пользователь может открыть только экран смены пароля и выполнить
logout; основная навигация не строится.
Роли, permissions и интерфейс¶
Frontend не принимает решений по именам ролей. Backend преобразует роль в набор типизированных permissions и должен возвращать их в auth-ответе. Интерфейс оперирует только permissions своего контура:
Проверки сосредоточены в небольшом общем слое каждого приложения:
useCan(permission)для программной проверки;<Can permission="...">для декларативного показа действия или блока;- route guard для защиты прямого перехода по URL;
- конфигурация маршрутов с требуемым permission, из которой также строится sidebar.
Одна конфигурация маршрутов определяет и доступность маршрута, и видимость пункта меню. Поэтому скрытие sidebar-пункта не является единственной проверкой, а правила не дублируются по компонентам.
Пример декларативного действия:
Компоненты не должны содержать проверки вида role === "ADMIN". Добавление или изменение роли на
backend не требует переписывать frontend, пока публичный набор permissions сохраняет смысл.
Auth-ответы обоих контуров содержат userId и permissions текущего principal. Frontend не должен
декодировать JWT и использовать его внутренние claims как публичный контракт сессии.
Действия, зависящие от конкретного объекта¶
Глобальный permission отвечает только на вопрос, может ли пользователь в принципе выполнять
операцию. Фактическая допустимость действия может зависеть от состояния и принадлежности объекта.
Например, ORDER_CANCEL не означает, что можно отменить уже завершённый заказ.
Для сущностей со сложным жизненным циклом backend возвращает вычисленный набор доступных действий:
Frontend показывает предметные действия по allowedActions, не воспроизводя серверную машину
состояний:
<EntityAction action="CANCEL" allowedActions={order.allowedActions}>
<Button danger>Отменить заказ</Button>
</EntityAction>
allowedActions является представлением текущего состояния, а не полномочием и не гарантией
успеха будущего запроса. Между загрузкой объекта и командой состояние может измениться, поэтому
backend при выполнении команды заново проверяет одновременно:
- подлинность и актуальность сессии;
- необходимый permission;
- область доступа и владение объектом;
- бизнес-допустимость перехода в его текущем состоянии.
Отказ backend остаётся источником истины. Frontend централизованно обрабатывает 403, 404 и
конфликт состояния, обновляет данные и показывает предметную ошибку. Скрытие кнопки, route guard и
allowedActions нужны для корректного UX и не считаются механизмами безопасности.
Контракт API¶
Типы запросов, ответов, permissions и allowedActions генерируются из опубликованного OpenAPI, а
не поддерживаются вручную в нескольких репозиториях. Изменение HTTP-контракта сначала отражается в
монолите и его OpenAPI, затем frontend обновляет сгенерированный клиент и адаптирует интерфейс.
Монолит публикует отдельную спецификацию для каждого frontend-контура:
Локальные команды task openapi:export-admin и task openapi:export-franchise сохраняют снимки в
web/build/openapi. Эти файлы являются генерируемыми артефактами и не фиксируются в Git.
Генератор и способ доставки OpenAPI фиксируются при bootstrap первого frontend-репозитория. В репозиторий не добавляется ручная копия backend enum, если тот же тип можно получить генерацией.
Минимальные проверки frontend¶
Каждое приложение должно проверять как минимум:
- восстановление сессии после перезагрузки и единичную ротацию при параллельных
401; - logout и переход на страницу входа после неуспешного refresh;
- изоляцию экрана обязательной смены временного пароля;
- отсутствие пунктов меню и действий без требуемого permission;
- запрет прямого перехода на недоступный маршрут;
- отображение действий по
allowedActions; - корректную обработку серверного отказа после устаревания
allowedActions.