feat(hooks): permission-driven gating via useHasPermission

Replace the rank-level family (useHasRankLevel + STAFF_LEVELS
constants + useIsRank) with a permission-driven family that reads
straight from the deployment's `permission_definitions` table — no
more hardcoded SecurityLevel/rank-id thresholds on the client. A new
rank in permission_ranks or a re-shuffling of permission_definitions
rank columns now propagates through the UI automatically.

Renderer-side wire shipped in companion commit
feat/react19-event-bus@159c5eb (UserPermissionsMapParser + Event,
SessionDataManager.getPermissionsSnapshot + USER_PERMISSIONS_UPDATED).

New public API in `useSessionSnapshots.ts`:
- useUserPermissions(): ReadonlyMap<string, number>  — full map
- useHasPermission(key): boolean                       — > 0 ⇒ true
- usePermissionValue(key): number                      — raw 1/2 or 0
- useIsAmbassador() now aliases useHasPermission('acc_ambassador')
- useUserRank() kept for PRESENTATIONAL use only (badge, prefix,
  prefix color) — documented as such in JSDoc; gating must use
  useHasPermission.

Dropped:
- src/api/nitro/session/RankLevels.ts (STAFF_LEVELS constants)
- useHasRankLevel / useIsRank exports (rank-based gating)

11 consumer migrations, each mapped to the right
`permission_definitions.permission_key`:

  - ToolbarView (mod-only chat-input button)        → acc_supporttool
  - ChooserWidgetView (room-object id column)       → acc_supporttool
  - CatalogClassicView (admin toggles)              → acc_catalogfurni
  - CatalogModernView (admin toggles)               → acc_catalogfurni
  - FurniEditorView (panel access)                  → acc_catalogfurni
  - CalendarView (force-open day)                   → acc_calendar_force
  - InfoStandWidgetFurniView (mod buildtools btn)   → acc_anyroomowner
  - AvatarInfoWidgetPetView (canPickUp)             → acc_anyroomowner
  - FurnitureMannequinView (controller mode)        → acc_anyroomowner
  - YouTubePlayerView (isMyRoom)                    → acc_anyroomowner
  - NavigatorRoomInfoView 'settings'                → acc_anyroomowner
  - NavigatorRoomInfoView 'staff_pick'              → acc_staff_pick

Test refresh:
- useUserRank still tested for the presentational shape.
- useHasPermission: true for non-zero, false for absent/zero.
- usePermissionValue: raw 1 / 2 / 0 (default).
- useUserPermissions: full map exposure.
- Runtime promote test: mutate the permissions map + dispatch
  USER_PERMISSIONS_UPDATED, assert useHasPermission flips false→true.
  Locks in the new reactive contract.

Mock unchanged (the test sets getPermissionsSnapshot via vi.mocked).

Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
214/214 (213 prior + 1 net new for useUserPermissions). Backward
compatible: older Arcturus deployments don't ship the map → empty
snapshot → every gate is false → mod UI hidden (safe default).
This commit is contained in:
simoleo89
2026-05-19 19:00:10 +02:00
parent 8aa02249e1
commit c7e258e3d1
15 changed files with 164 additions and 127 deletions
@@ -1,9 +1,9 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react';
import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa';
import { CatalogType, GetConfigurationValue, LocalizeText, STAFF_LEVELS } from '../../api';
import { CatalogType, GetConfigurationValue, LocalizeText } from '../../api';
import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { useCatalogActions, useCatalogData, useCatalogUiState, useHasRankLevel } from '../../hooks';
import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission } from '../../hooks';
import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext';
import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView';
import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView';
@@ -28,7 +28,7 @@ const CatalogClassicViewInner: FC<{}> = () =>
{});
const loading = catalogAdmin?.loading ?? false;
const isMod = useHasRankLevel(STAFF_LEVELS.MOD);
const isMod = useHasPermission('acc_catalogfurni');
const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER)
? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' }
: undefined;