UI-гейтинг по permissions

(Введено в BR 1.5)

Принцип

Нет section.read → раздел скрыт. Есть read без edit → read-only страница (данные видны, кнопки действий скрыты). Backend — source of truth; фронт гейтит UI, бэк всегда проверяет (defence in depth).


1. Sidebar гейтинг

Каждый пункт бокового меню привязан к permission. Нет permission → пункт не рендерится.

Таблица пунктов меню

РазделPermission для видимостиПримечание
Дашборд— (всегда видно)Fallback-страница для всех
Юр. лицаlegal_entities.read+ скрыто при franchise.type=individual
Торговые точкиstores.read
Каталог (группа)Видна если хотя бы один подпункт доступен
— Товарыmenu.read
— Категорииmenu.read
— Модификаторыmenu.read
— Прейскурантыprice_list.read
— Ингредиентыingredients.read
— Стоп-листыstoplists.read
Склад (группа)Видна если хотя бы один подпункт доступен
— Остаткиwarehouse.read
— Приёмкиwarehouse.read
— Списанияwarehouse.read
Сотрудники (группа)Видна если хотя бы один подпункт доступен
— Списокemployees.read
— Ролиroles.read
— Расписаниеschedule.read
— Учёт рабочего времениtime_tracking.read
— Зарплата (формулы)payroll.read
— Зарплата (ведомости)payroll.read
Заказы (группа)
— Активныеorders.read
— Историяorders.read
Клиенты (группа)Видна если хотя бы один подпункт доступен (BR 3.1)
— Клиентыcustomers.readПункт меню «Клиенты»
— Группы клиентовcustomer_groups.readПункт меню «Группы клиентов» (только владелец франшизы — у партнёра customer_groups.read обычно не выдаётся)
Отчётыreports.read

Правила

  • Дашборд всегда видно — fallback при отсутствии прав на другие разделы
  • Группа (Каталог, Склад, Сотрудники, Заказы) видна если >= 1 подпункт виден. Пример: нет menu.read, но есть price_list.read → «Каталог» виден, внутри только «Прейскуранты»
  • Franchise owner (scope.type = all_franchise): bypass — видит ВСЁ, проверки пропускаются (usePermission hook уже реализует это)

2. Кнопки на страницах

На каждой странице кнопки действий скрываются если нет соответствующего .edit permission.

Read без Edit = read-only

Если есть section.read без section.edit → страница открывается, данные видны, но все кнопки создания/редактирования/удаления скрыты, формы заблокированы.

Полная таблица

СтраницаКнопкиPermission
ТТ: список«Добавить»stores.edit
ТТ: карточка«Редактировать», «Удалить», «Опубликовать/Снять»stores.edit
Каталог: товары список«Добавить товар»menu.edit
Каталог: товар карточка«Редактировать», «Удалить»menu.edit
Каталог: категории«Добавить», drag-n-dropmenu.edit
Каталог: модификаторы список«Добавить»menu.edit
Каталог: модификатор карточка«Редактировать», «Удалить»menu.edit
Прейскуранты: список«Добавить»price_list.edit
Прейскуранты: карточка«Редактировать»price_list.edit
Ингредиенты: список«Добавить»ingredients.edit
Ингредиенты: карточка«Редактировать»ingredients.edit
Стоп-листыToggle on/offstoplists.edit
Склад: приёмки«Добавить акт»warehouse.edit
Склад: списания«Добавить акт»warehouse.edit
Сотрудники: список«Добавить», меню действийemployees.edit
Сотрудники: карточка«Редактировать», «Деактивировать/Реактивировать»employees.edit
Роли: список«Добавить», «Удалить»roles.edit
Роли: карточка«Редактировать», «Удалить»roles.edit
Расписание«Добавить смену»schedule.edit
Зарплата: формулы«Добавить», «Редактировать»payroll.edit
Зарплата: ведомости«Рассчитать», «Подтвердить», «Отметить выплаченной»payroll.edit
ЗаказыЗависит от floworders.edit
ЮЛ: список«Добавить»legal_entities.edit
ЮЛ: карточка«Редактировать», «Удалить»legal_entities.edit

3. Route guard

Компонент <PermissionRoute permission="section.read"> оборачивает маршрут. Если у пользователя нет нужного permission — рендерит страницу «Нет доступа» вместо children.

Поведение

  1. Пользователь переходит по URL (закладка, прямая ссылка)
  2. <PermissionRoute> проверяет usePermission(permission)
  3. Если permission есть → рендерит children (страницу)
  4. Если permission нет → рендерит <NoAccessPage />

ASCII-макет страницы «Нет доступа» (403)

┌──────────────────────────────────────────┐
│  [Sidebar]  │                            │
│             │                            │
│  Dashboard  │    ┌────────────────────┐   │
│  ...        │    │   🔒               │   │
│             │    │                    │   │
│             │    │   У вас нет        │   │
│             │    │   доступа к этому  │   │
│             │    │   разделу          │   │
│             │    │                    │   │
│             │    │  [ На главную ]    │   │
│             │    └────────────────────┘   │
│             │                            │
└──────────────────────────────────────────┘
  • Иконка замка
  • Текст: «У вас нет доступа к этому разделу»
  • Кнопка «На главную» → переход на / (дашборд)

4. POS-only блокировка

Если у сотрудника нет ни одного backoffice *.read permission (только POS-операции: pos.access, pos.shift.open и т.д.) — вход в бэк-офис запрещён.

Сценарий

  1. Сотрудник вводит email + пароль на /login
  2. Фронт отправляет POST /auth/login
  3. Вариант A (бэкенд): Auth Service проверяет permissions при логине. Если нет ни одного backoffice *.read → возвращает ошибку NO_BACKOFFICE_ACCESS (403). Фронт показывает сообщение.
  4. Вариант B (фронтенд): Login успешен → фронт вызывает GET /auth/me → получает permissions[] → если нет ни одного backoffice *.read (кроме pos.*) → показывает модалку и не пускает дальше.

Реализовать оба варианта

Бэкенд возвращает NO_BACKOFFICE_ACCESS (defence in depth). Фронт дополнительно проверяет после /auth/me (для случаев когда бэкенд ещё не обновлён или permissions изменились после логина).

Модалка POS-only

┌─────────────────────────────────────┐
│                                     │
│   У вашей роли нет доступа          │
│   к бэк-офису.                      │
│                                     │
│   Используйте POS-приложение.       │
│                                     │
│            [ Выйти ]                │
│                                     │
└─────────────────────────────────────┘
  • Модалка без возможности закрыть (нет крестика)
  • Единственное действие — кнопка «Выйти» → logout + redirect на /login

5. Обработка 403 от API

При получении HTTP 403 FORBIDDEN от любого бэкенд-запроса:

  • api/client.ts перехватывает 403 в response interceptor
  • Показывает toast: «У вас нет прав на это действие»
  • Не редиректит — пользователь остаётся на текущей странице
  • Toast исчезает через 5 секунд (стандартное поведение)

Не путать с 401

401 (UNAUTHORIZED) → redirect на /login (токен истёк). 403 (FORBIDDEN) → toast (прав нет, но сессия валидна).


6. Состояния

СостояниеОписаниеЧто видит пользователь
Loading permissions/auth/me ещё не вернул ответ после логинаSidebar в skeleton-режиме (серые плашки вместо пунктов меню). Контент-область — спиннер
POS-only blockНет ни одного backoffice *.readМодалка «У вашей роли нет доступа к бэк-офису» с кнопкой «Выйти»
403 pageПрямой переход по URL без permissionСтраница «У вас нет доступа к этому разделу» + кнопка «На главную»
403 toastБэкенд вернул 403 на действиеToast «У вас нет прав на это действие» (5 сек)
Read-only modeЕсть .read без .editСтраница отображает данные, но кнопки create/edit/delete скрыты

7. Ссылки