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/, фільтрує:
- Section без installed
appCode→ виключити (license gate) - Item з
pluginключем — лише якщо плагін активний (license gate) - Item з
requiresMultitenant: true— лише в multi-tenant build - Item permission gate (новий, B → useActivePermissions):
- Якщо
item.requiresEntitiesзадано → дозволено якщоcanView()хоча б на одну entity з масиву - Інакше fallback per-type:
- MASTERDATA / TRANSACTIONDATA / JOURNAL / LEDGER →
canView(item.code) - REPORT →
canView('reports')(псевдо-entity) - APPLICATION / PROCESS без
requiresEntities→ closed-by-default (приховано від не-admin)
- MASTERDATA / TRANSACTIONDATA / JOURNAL / LEDGER →
- 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 branches gating¶
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