Перейти к содержанию

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 своего контура:

роль на backend → permissions текущей сессии → доступность frontend-возможностей

Проверки сосредоточены в небольшом общем слое каждого приложения:

  • useCan(permission) для программной проверки;
  • <Can permission="..."> для декларативного показа действия или блока;
  • route guard для защиты прямого перехода по URL;
  • конфигурация маршрутов с требуемым permission, из которой также строится sidebar.

Одна конфигурация маршрутов определяет и доступность маршрута, и видимость пункта меню. Поэтому скрытие sidebar-пункта не является единственной проверкой, а правила не дублируются по компонентам.

Пример декларативного действия:

<Can permission="ORDER_CANCEL">
  <Button danger onClick={cancelOrder}>Отменить заказ</Button>
</Can>

Компоненты не должны содержать проверки вида role === "ADMIN". Добавление или изменение роли на backend не требует переписывать frontend, пока публичный набор permissions сохраняет смысл.

Auth-ответы обоих контуров содержат userId и permissions текущего principal. Frontend не должен декодировать JWT и использовать его внутренние claims как публичный контракт сессии.

Действия, зависящие от конкретного объекта

Глобальный permission отвечает только на вопрос, может ли пользователь в принципе выполнять операцию. Фактическая допустимость действия может зависеть от состояния и принадлежности объекта. Например, ORDER_CANCEL не означает, что можно отменить уже завершённый заказ.

Для сущностей со сложным жизненным циклом backend возвращает вычисленный набор доступных действий:

{
  "id": "123",
  "status": "COMPLETED",
  "allowedActions": ["VIEW"]
}

Frontend показывает предметные действия по allowedActions, не воспроизводя серверную машину состояний:

<EntityAction action="CANCEL" allowedActions={order.allowedActions}>
  <Button danger>Отменить заказ</Button>
</EntityAction>

allowedActions является представлением текущего состояния, а не полномочием и не гарантией успеха будущего запроса. Между загрузкой объекта и командой состояние может измениться, поэтому backend при выполнении команды заново проверяет одновременно:

  1. подлинность и актуальность сессии;
  2. необходимый permission;
  3. область доступа и владение объектом;
  4. бизнес-допустимость перехода в его текущем состоянии.

Отказ backend остаётся источником истины. Frontend централизованно обрабатывает 403, 404 и конфликт состояния, обновляет данные и показывает предметную ошибку. Скрытие кнопки, route guard и allowedActions нужны для корректного UX и не считаются механизмами безопасности.

Контракт API

Типы запросов, ответов, permissions и allowedActions генерируются из опубликованного OpenAPI, а не поддерживаются вручную в нескольких репозиториях. Изменение HTTP-контракта сначала отражается в монолите и его OpenAPI, затем frontend обновляет сгенерированный клиент и адаптирует интерфейс.

Монолит публикует отдельную спецификацию для каждого frontend-контура:

/v3/api-docs/admin      → только /api/admin/**
/v3/api-docs/franchise  → только /api/franchise/**

Локальные команды task openapi:export-admin и task openapi:export-franchise сохраняют снимки в web/build/openapi. Эти файлы являются генерируемыми артефактами и не фиксируются в Git.

Генератор и способ доставки OpenAPI фиксируются при bootstrap первого frontend-репозитория. В репозиторий не добавляется ручная копия backend enum, если тот же тип можно получить генерацией.

Минимальные проверки frontend

Каждое приложение должно проверять как минимум:

  • восстановление сессии после перезагрузки и единичную ротацию при параллельных 401;
  • logout и переход на страницу входа после неуспешного refresh;
  • изоляцию экрана обязательной смены временного пароля;
  • отсутствие пунктов меню и действий без требуемого permission;
  • запрет прямого перехода на недоступный маршрут;
  • отображение действий по allowedActions;
  • корректную обработку серверного отказа после устаревания allowedActions.