RBAC-гейт в UI
Как показывать и прятать элементы UI по ролям пользователя — кнопки, пункты меню, целые экраны. Рецепт строится на хуке useCan (и чистой функции can) из @mineflow/client-react, 7 канонических ролях SystemRole и маппинге Keycloak-ролей через mapKeycloakRoles.
useCan/can управляют только видимостью в интерфейсе. Это удобство (не показывать кнопку, которую всё равно нельзя нажать), а не контроль доступа. Сервер заново проверяет роли и object-scope по JWT и вернёт 403, если запрос недопустим. Никогда не полагайтесь на UI-гейт как на единственную защиту: пользователь может выполнить запрос мимо вашего UI.
7 канонических ролей
Источник истины — бэкенд (SystemRole), на фронт тип приходит без дрейфа. Канонические роли всегда в PascalCase:
import type { SystemRole } from '@mineflow/client-core';
// 'CEO' | 'Engineer' | 'Foreman' | 'Mechanic' | 'Admin' | 'OtibSpecialist' | 'Supply'
| Роль | Назначение |
|---|---|
CEO | Руководство |
Engineer | Инженер БВР |
Foreman | Бригадир (сменные рапорты) |
Mechanic | Механик (обслуживание техники) |
Admin | Администратор системы |
OtibSpecialist | Специалист ОТиБ |
Supply | Снабжение (ТМЦ, ГСМ) |
В Keycloak realm-роли пишутся в lowercase (foreman, otib), но во фронте используются только канонические PascalCase-имена. Конвертация — разовая, через mapKeycloakRoles (см. ниже). Передавать в useCan строки вида 'foreman' нельзя — TypeScript не пропустит, а на рантайме гейт молча не сработает.
useCan — гейт в компоненте
useCan(required) берёт роли текущего пользователя из <MineflowProvider> и возвращает boolean: есть ли у пользователя хотя бы одна из требуемых ролей (OR-семантика).
import { useCan } from '@mineflow/client-react';
function ApproveButton({ reportId }: { reportId: string }) {
// Утвердить рапорт могут CEO и Engineer
const canApprove = useCan(['CEO', 'Engineer']);
if (!canApprove) return null;
return <button onClick={() => approve(reportId)}>Утвердить</button>;
}
useCan([]) (и can(roles, [])) всегда возвращает true. Это сознательное поведение: «требуется только вход в систему, конкретная роль не важна». Используйте для элементов, доступных всем залогиненным пользователям.
const canSeeProfile = useCan([]); // true для любого аутентифицированного
useCan работает только внутри <MineflowProvider> — он читает roles из контекста (см. гайд client-react). Вне провайдера хук бросит ошибку useMineflow must be used within <MineflowProvider>.
can — чистая функция вне React
Если роли уже на руках (например, в роутер-guard, в утилите или в тесте), берите чистую функцию can(userRoles, required) из того же пакета — она не требует контекста и React:
import { can } from '@mineflow/client-react';
import type { SystemRole } from '@mineflow/client-core';
function guardRoute(userRoles: readonly SystemRole[]): boolean {
// секция снабжения: Supply или Admin
return can(userRoles, ['Supply', 'Admin']);
}
useCan — это просто can(roles из контекста, required), так что семантика идентична: OR между ролями, пустой required → true.
Откуда берутся роли: mapKeycloakRoles
<MineflowProvider> ждёт роли в каноническом PascalCase. Auth-адаптер (@mineflow/auth-web для web, @mineflow/auth-native для RN) обычно резолвит их сам через getRoles(). Под капотом lowercase realm-роли Keycloak конвертируются в SystemRole функцией mapKeycloakRoles — она же реэкспортируется из auth-web для платформенного паритета:
import { mapKeycloakRoles } from '@mineflow/client-core';
// realm-роли из JWT (lowercase) → канонические SystemRole, с дедупликацией
mapKeycloakRoles(['foreman', 'otib', 'unknown']); // → ['Foreman', 'OtibSpecialist']
Неизвестные realm-роли (нет в маппинге) просто отбрасываются. Дубликаты схлопываются.
import Keycloak from 'keycloak-js';
import { KeycloakTokenProvider } from '@mineflow/auth-web';
import { MineflowProvider } from '@mineflow/client-react';
const tokenProvider = new KeycloakTokenProvider({ keycloak });
function App() {
return (
<MineflowProvider
baseUrl={import.meta.env.VITE_API_BASE}
tokenProvider={tokenProvider}
generateId={() => crypto.randomUUID()}
roles={tokenProvider.getRoles()} // уже PascalCase SystemRole[]
>
<Screens />
</MineflowProvider>
);
}
mapKeycloakRoles живёт в platform-agnostic ядре @mineflow/client-core (используется и web-, и RN-адаптерами). Это единственная допущенная копия с ерверного маппинга — она покрыта тестом, который сторожит все 7 ролей. Если на бэке появится 8-я роль, маппинг и тип SystemRole обновятся синхронно.
Рецепты по сценариям
Прятать кнопку
function DecommissionButton() {
const canDecommission = useCan(['Engineer', 'Admin']);
return canDecommission ? <button>Списать актив</button> : null;
}
Дизейблить вместо скрытия
Иногда лучше показать действие, но заблокировать его (чтобы пользователь видел, что функция существует):
function RegisterAssetButton() {
const canRegister = useCan(['Engineer', 'Admin']);
return (
<button disabled={!canRegister} title={canRegister ? '' : 'Недостаточно прав'}>
Зарегистрировать
</button>
);
}
Гейт целого экрана
function SupplyScreen() {
const canEnter = useCan(['Supply', 'Admin']);
if (!canEnter) return <AccessDenied />;
return <SupplyDashboard />;
}
Комбинация с FSM-видимостью
RBAC и состояние сущности — независимые оси. Кнопку показываем, только если переход доступен в текущем статусе (FSM) и у пользователя есть роль:
import { useAvailableActions, useCan } from '@mineflow/client-react';
import { shiftReportMachine } from '@mineflow/api-schemas';
function ApproveAction({ status }: { status: string }) {
const actions = useAvailableActions(shiftReportMachine, status);
const canApprove = useCan(['CEO', 'Engineer']);
if (!actions.includes('APPROVE') || !canApprove) return null;
return <button>Утвердить</button>;
}
Подробнее про FSM-кнопки — в рецепте FSM-кнопки в UI.
useCan проверяет только наличие роли, но не объектную привязку. Например, Foreman может редактировать рапорты своих участков, но не чужих — это object-scope, его enforce'ит только сервер (по JWT, без org_id/scope в URL). На отказ по scope сервер вернёт 403 — обрабатывайте его как обычную ошибку (см. рецепт Обработка ошибок), не пытайтесь воспроизвести scope-логику на фронте.
Что дальше
- Обработка ошибок — как ловить 403 от сервера (источник истины по доступу).
- FSM-кнопки в UI — вторая ось видимости действий.
- Гайд client-react — провайдер, контекст, полный набор хуков.
- API-референс:
@mineflow/client-react(useCan,can),@mineflow/client-core(SystemRole,mapKeycloakRoles,KEYCLOAK_ROLE_ALIASES). - REST-эндпоинты и их роли — REST-референс.