Перейти до змісту

Frontend integration

Точка входу: /auth/me/

Backend CurrentUserView повертає поле permissions:

{
  "id": 7,
  "username": "melnyk",
  "is_tenant_admin": false,
  "permissions": {
    "wildcard": false,
    "entries": [
      ["invoices", "view"],
      ["invoices", "create"],
      ["invoices", "post"],
      // ... ще 251 запис
    ]
  }
}

wildcard=true для is_superuser / is_tenant_admin (тоді entries: [['*', 'all']] як indication, фактично frontend skip-ить filtering).

Цей запит виконує useCurrentUser при кожному завантаженні сторінки. Результат йде в useAuthStore.user.permissions.

Hook: useActivePermissions

useActivePermissions — основний API для перевірки прав на frontend.

const perms = useActivePermissions();

perms.canView('invoices')      // true | false
perms.canCreate('invoices')    // true | false
perms.canUpdate('invoices')    // true | false
perms.canDelete('invoices')    // true | false
perms.canPost('invoices')      // true | false
perms.has('invoices', 'view')  // generic — same as canView
perms.isWildcard               // true для tenant_admin/superuser
perms.set                      // Set<"entity:action"> для дебагу

Контракт: - wildcard=true → усі can*() повертають true. - Каталог wildcards ((entity, 'all')) теж покриваються. - Лукап — O(1) через внутрішній Set<string>. - Memoized через useMemo; перерахунок лише коли user.permissions змінюється.

Приклад використання у компоненті:

import { useActivePermissions } from '@/hooks/useActivePermissions';

function InvoiceToolbar() {
  const perms = useActivePermissions();
  return (
    <Group>
      <Button disabled={!perms.canCreate('invoices')}>Новий рахунок</Button>
      <Button disabled={!perms.canDelete('invoices')} color="red">Видалити</Button>
      <Button disabled={!perms.canPost('invoices')}>Провести</Button>
    </Group>
  );
}

Hook: useFilteredSections

useFilteredSections — драйвер sidebar accordion і section-dashboard. Бере статичний sections з config/, фільтрує:

  1. Section без installed appCode → виключити (license gate)
  2. Item з plugin ключем — лише якщо плагін активний (license gate)
  3. Item з requiresMultitenant: true — лише в multi-tenant build
  4. Item permission gate (новий, B → useActivePermissions):
  5. Якщо item.requiresEntities задано → дозволено якщо canView() хоча б на одну entity з масиву
  6. Інакше fallback per-type:
    • MASTERDATA / TRANSACTIONDATA / JOURNAL / LEDGER → canView(item.code)
    • REPORT → canView('reports') (псевдо-entity)
    • APPLICATION / PROCESS без requiresEntitiesclosed-by-default (приховано від не-admin)
  7. Subgroups/Groups/Sections без жодного дозволеного item → колапсуються

Tenant-admin / superuser обходить permission filter (бо isWildcard=true повертає true зі всіх checks).

requiresEntities — opt-in для PROCESS / APPLICATION

PROCESS-items (Bank Exchange wizard, Month Closing, Depreciation Run) мають code що не відповідає реальній entity. Аналогічно APPLICATION items (appSalesField, appBankExchange).

Без явного гейту вони стануть невидимі для всіх крім tenant_admin (closed-by-default). Щоб дати доступ — додай requiresEntities у конфізі:

// frontend/erp/src/config/essentials.ts
{
  code: "monthClosing",
  type: ItemType.PROCESS,
  requiresEntities: ['monthClosings'],  // ← користувач бачить пункт якщо має view на monthClosings
  // ... решта
}

Семантика: дозволено якщо canView() повертає true хоча б на одну entity з масиву.

Для APPLICATION items без власної секції (наприклад appSalesField із plugin: 'sales_field') — фільтр у Sidebar.tsx додатково перевіряє reachablePluginKeys: app з plugin: 'X' зникає, якщо у дозволених sections немає жодного item з тим самим plugin: 'X'.

Sidebar.tsx робить останній фільтр над списком installed apps:

Step 1: license gate — app installed for tenant
Step 2a: app має власну секцію (appCode-binding) → секція має бути в filteredSections
Step 2b: app — pluginbased без секції → mais existed item з тим самим plugin key у filteredSections
Step 2c: app без секції і без plugin (рідкісний) → keep (AppStore meta)

Тобто при логіні engineer'a не лише ховаються item у Essentials, а й увесь модуль appSalesField зникає з лівого accordion'у — бо у engineer'а немає жодного item з plugin: 'sales_field'.

Налагодження

"Чому я не бачу пункт меню?" 1. Перевірити is_tenant_admin — якщо true, то точно повинно бути видно. Якщо ні — питання прав. 2. Дізнатись entity_code пункту: для MASTERDATA/TRANSACTIONDATA — це code. Для PROCESS/APPLICATION — requiresEntities. 3. У DevTools: useAuthStore.getState().user?.permissions?.entries — список дозволених [entity, action]. 4. Якщо потрібна canView('X') і у списку нема ні [X, view] ні [X, all] ні ['*', 'all'] → права немає, треба додати у каталог.

"Backend каже 403, fronent показує." - Або memoization у hook: hard-reload (Ctrl+Shift+R) щоб оновити /auth/me/. - Або item не має requiresEntities → frontend прийняв fallback "не блокую", а backend гарантує гейт.

"Backend каже 200, frontend ховає." - Перевір requiresEntities у конфізі — мабуть пропустив реальну entity, на яку юзер має view.

Test coverage

  • dependencies.test.ts — 23 vitest-кейси покривають buildSidebarTree. Permission filter тестується через окремий test set у тому ж модулі.
  • Backend smoke-тести під bondarenko/admin, melnyk/admin, gate/admin — у docs/eswf/permissions/role-catalog.md є приклади.

Connection до seed_demo

Для тестування різних ролей у dev — використовуй демо-логіни з demo-users.md. Усі мають пароль admin. Перевір UI під різними юзерами щоб переконатись: - bondarenko (engineer) бачить тільки HR + ATtendance - melnyk (accountant + sales_manager) бачить майже все Essentials + CRM - gate (gate_operator) бачить тільки Gatehouse + vehicle ref data