@jmruthers/pace-core 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (549) hide show
  1. package/CHANGELOG.md +88 -10
  2. package/cursor-rules/00-pace-core-compliance.mdc +46 -87
  3. package/cursor-rules/01-standards-compliance.mdc +16 -47
  4. package/cursor-rules/02-project-structure.mdc +4 -4
  5. package/cursor-rules/03-solid-principles.mdc +45 -164
  6. package/cursor-rules/04-testing-standards.mdc +22 -69
  7. package/cursor-rules/05-bug-reports-and-features.mdc +2 -2
  8. package/cursor-rules/06-code-quality.mdc +42 -125
  9. package/cursor-rules/07-tech-stack-compliance.mdc +33 -128
  10. package/cursor-rules/08-markup-quality.mdc +452 -0
  11. package/cursor-rules/CHANGELOG.md +18 -0
  12. package/cursor-rules/README.md +2 -1
  13. package/dist/{AuthService-DjnJHDtC.d.ts → AuthService-Cb34EQs3.d.ts} +63 -1
  14. package/dist/{DataTable-CH1U5Tpy.d.ts → DataTable-BMRU8a1j.d.ts} +33 -1
  15. package/dist/{DataTable-DQ7RSOHE.js → DataTable-THFPBKTP.js} +12 -10
  16. package/dist/{PublicPageProvider-ce4xlHYA.d.ts → PublicPageProvider-DEMpysFR.d.ts} +394 -171
  17. package/dist/{UnifiedAuthProvider-185Ih4dj.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +30 -8
  18. package/dist/{UnifiedAuthProvider-ATAP5UTR.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
  19. package/dist/{api-N774RPUA.js → api-IAGWF3ZG.js} +10 -10
  20. package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
  21. package/dist/{chunk-JBKQ3SAO.js → chunk-2T2IG7T7.js} +107 -57
  22. package/dist/chunk-2T2IG7T7.js.map +1 -0
  23. package/dist/{chunk-3QRJFVBR.js → chunk-6SOIHG6Z.js} +1 -1
  24. package/dist/chunk-6SOIHG6Z.js.map +1 -0
  25. package/dist/{chunk-3XTALGJF.js → chunk-6Z7LTB3D.js} +69 -240
  26. package/dist/chunk-6Z7LTB3D.js.map +1 -0
  27. package/dist/{chunk-4ZC4GX36.js → chunk-CNCQDFLN.js} +199 -46
  28. package/dist/chunk-CNCQDFLN.js.map +1 -0
  29. package/dist/chunk-DGUM43GV.js +11 -0
  30. package/dist/{chunk-BYFSK72L.js → chunk-DWUBLJJM.js} +361 -187
  31. package/dist/chunk-DWUBLJJM.js.map +1 -0
  32. package/dist/{chunk-LXQLPRQ2.js → chunk-FFQEQTNW.js} +6 -8
  33. package/dist/chunk-FFQEQTNW.js.map +1 -0
  34. package/dist/chunk-FMUCXFII.js +76 -0
  35. package/dist/chunk-FMUCXFII.js.map +1 -0
  36. package/dist/{chunk-4N5C5XZU.js → chunk-HFZBI76P.js} +4 -4
  37. package/dist/chunk-HFZBI76P.js.map +1 -0
  38. package/dist/{chunk-SQGMNID3.js → chunk-L4OXEN46.js} +4 -5
  39. package/dist/chunk-L4OXEN46.js.map +1 -0
  40. package/dist/{chunk-R77UEZ4E.js → chunk-M43Y4SSO.js} +1 -1
  41. package/dist/chunk-M43Y4SSO.js.map +1 -0
  42. package/dist/{chunk-I7PSE6JW.js → chunk-M7MPQISP.js} +3 -76
  43. package/dist/chunk-M7MPQISP.js.map +1 -0
  44. package/dist/chunk-PQBSKX33.js +7793 -0
  45. package/dist/chunk-PQBSKX33.js.map +1 -0
  46. package/dist/chunk-QRPVRXYT.js +226 -0
  47. package/dist/chunk-QRPVRXYT.js.map +1 -0
  48. package/dist/{chunk-KNC55RTG.js → chunk-RWEBCB47.js} +194 -416
  49. package/dist/chunk-RWEBCB47.js.map +1 -0
  50. package/dist/{chunk-XM25TVIE.js → chunk-YDQHOZNA.js} +843 -388
  51. package/dist/chunk-YDQHOZNA.js.map +1 -0
  52. package/dist/{chunk-GLK6VM3F.js → chunk-ZNIWI3UC.js} +739 -737
  53. package/dist/chunk-ZNIWI3UC.js.map +1 -0
  54. package/dist/components.d.ts +5 -5
  55. package/dist/components.js +18 -16
  56. package/dist/components.js.map +1 -1
  57. package/dist/contextValidator-3JNZKUTX.js +9 -0
  58. package/dist/contextValidator-3JNZKUTX.js.map +1 -0
  59. package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
  60. package/dist/{functions-D_kgHktt.d.ts → functions-DHebl8-F.d.ts} +1 -1
  61. package/dist/hooks.d.ts +55 -122
  62. package/dist/hooks.js +10 -13
  63. package/dist/hooks.js.map +1 -1
  64. package/dist/index.d.ts +60 -13
  65. package/dist/index.js +30 -25
  66. package/dist/index.js.map +1 -1
  67. package/dist/providers.d.ts +21 -3
  68. package/dist/providers.js +4 -3
  69. package/dist/rbac/index.d.ts +210 -139
  70. package/dist/rbac/index.js +17 -13
  71. package/dist/styles/index.js +1 -1
  72. package/dist/theming/runtime.d.ts +1 -13
  73. package/dist/theming/runtime.js +2 -2
  74. package/dist/{timezone-_pgH8qrY.d.ts → timezone-CHhWg6b4.d.ts} +3 -10
  75. package/dist/{types-UU913iLA.d.ts → types-BeoeWV5I.d.ts} +8 -0
  76. package/dist/{types-CEpcvwwF.d.ts → types-CkbwOr4Y.d.ts} +6 -0
  77. package/dist/types.d.ts +2 -2
  78. package/dist/types.js +1 -1
  79. package/dist/{usePublicRouteParams-BJAlWfuJ.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +38 -17
  80. package/dist/utils.d.ts +4 -5
  81. package/dist/utils.js +17 -19
  82. package/dist/utils.js.map +1 -1
  83. package/docs/api/README.md +21 -17
  84. package/docs/api/modules.md +4191 -2967
  85. package/docs/architecture/database-schema-requirements.md +161 -0
  86. package/docs/components/context-selector.md +126 -0
  87. package/docs/core-concepts/rbac-system.md +3 -3
  88. package/docs/documentation-index.md +2 -4
  89. package/docs/getting-started/cursor-rules.md +2 -1
  90. package/docs/migration/DOCUMENTATION_STRUCTURE.md +441 -0
  91. package/docs/migration/MIGRATION_GUIDE.md +2 -24
  92. package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
  93. package/docs/migration/README.md +52 -6
  94. package/docs/migration/V0.5.190_TO_V0.6.1_MIGRATION.md +1153 -0
  95. package/docs/migration/database-changes-december-2025.md +3 -3
  96. package/docs/pace-mint-fix-auto-selection.md +218 -0
  97. package/docs/pace-mint-rbac-setup.md +391 -0
  98. package/docs/rbac/event-based-apps.md +1 -1
  99. package/docs/rbac/getting-started.md +1 -1
  100. package/docs/rbac/quick-start.md +1 -1
  101. package/docs/rbac/secure-client-protection.md +330 -0
  102. package/docs/standards/README.md +1 -0
  103. package/package.json +4 -3
  104. package/scripts/audit/core/checks/accessibility.cjs +197 -0
  105. package/scripts/audit/core/checks/api-usage.cjs +191 -0
  106. package/scripts/audit/core/checks/bundle.cjs +142 -0
  107. package/scripts/{check-pace-core-compliance.cjs → audit/core/checks/compliance.cjs} +784 -685
  108. package/scripts/audit/core/checks/config.cjs +54 -0
  109. package/scripts/audit/core/checks/coverage.cjs +84 -0
  110. package/scripts/audit/core/checks/dependencies.cjs +985 -0
  111. package/scripts/audit/core/checks/documentation.cjs +268 -0
  112. package/scripts/audit/core/checks/environment.cjs +116 -0
  113. package/scripts/audit/core/checks/error-handling.cjs +340 -0
  114. package/scripts/audit/core/checks/forms.cjs +172 -0
  115. package/scripts/audit/core/checks/heuristics.cjs +68 -0
  116. package/scripts/audit/core/checks/hooks.cjs +334 -0
  117. package/scripts/audit/core/checks/imports.cjs +244 -0
  118. package/scripts/audit/core/checks/performance.cjs +325 -0
  119. package/scripts/audit/core/checks/routes.cjs +117 -0
  120. package/scripts/audit/core/checks/state.cjs +130 -0
  121. package/scripts/audit/core/checks/structure.cjs +65 -0
  122. package/scripts/audit/core/checks/style.cjs +584 -0
  123. package/scripts/audit/core/checks/testing.cjs +122 -0
  124. package/scripts/audit/core/checks/typescript.cjs +61 -0
  125. package/scripts/audit/core/scanner.cjs +199 -0
  126. package/scripts/audit/core/utils.cjs +137 -0
  127. package/scripts/audit/index.cjs +223 -0
  128. package/scripts/audit/reporters/console.cjs +151 -0
  129. package/scripts/audit/reporters/json.cjs +54 -0
  130. package/scripts/audit/reporters/markdown.cjs +124 -0
  131. package/scripts/audit-consuming-app.cjs +61 -936
  132. package/scripts/build-docs/build-decision.js +240 -0
  133. package/scripts/build-docs/cache-utils.js +105 -0
  134. package/scripts/build-docs/content-normalization.js +150 -0
  135. package/scripts/build-docs/file-utils.js +105 -0
  136. package/scripts/build-docs/git-utils.js +86 -0
  137. package/scripts/build-docs/hash-utils.js +116 -0
  138. package/scripts/build-docs/typedoc-runner.js +220 -0
  139. package/scripts/build-docs-incremental.js +77 -913
  140. package/scripts/utils/command-runner.js +16 -11
  141. package/scripts/validate-formats.js +61 -56
  142. package/scripts/validate-master.js +74 -69
  143. package/scripts/validate-pre-publish.js +70 -65
  144. package/src/__tests__/hooks/usePermissions.test.ts +2 -2
  145. package/src/components/Alert/Alert.test.tsx +12 -18
  146. package/src/components/Alert/Alert.tsx +5 -7
  147. package/src/components/Avatar/Avatar.test.tsx +4 -4
  148. package/src/components/Badge/Badge.tsx +14 -0
  149. package/src/components/Button/Button.tsx +22 -0
  150. package/src/components/Calendar/Calendar.tsx +8 -2
  151. package/src/components/Card/Card.tsx +4 -0
  152. package/src/components/Checkbox/Checkbox.test.tsx +12 -12
  153. package/src/components/Checkbox/Checkbox.tsx +2 -2
  154. package/src/components/ContextSelector/ContextSelector.tsx +384 -0
  155. package/src/components/ContextSelector/index.ts +3 -0
  156. package/src/components/DataTable/DataTable.tsx +38 -4
  157. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +5 -6
  158. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +18 -4
  159. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +2 -3
  160. package/src/components/DataTable/components/AccessDeniedPage.tsx +16 -25
  161. package/src/components/DataTable/components/ActionButtons.tsx +10 -7
  162. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
  163. package/src/components/DataTable/components/ColumnFilter.tsx +10 -0
  164. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +12 -0
  165. package/src/components/DataTable/components/DataTableBody.tsx +8 -0
  166. package/src/components/DataTable/components/DataTableCore.tsx +196 -554
  167. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +11 -0
  168. package/src/components/DataTable/components/DataTableLayout.tsx +559 -0
  169. package/src/components/DataTable/components/DataTableModals.tsx +8 -0
  170. package/src/components/DataTable/components/DataTableToolbar.tsx +8 -0
  171. package/src/components/DataTable/components/DraggableColumnHeader.tsx +12 -0
  172. package/src/components/DataTable/components/EditFields.tsx +307 -0
  173. package/src/components/DataTable/components/EditableRow.tsx +8 -0
  174. package/src/components/DataTable/components/EmptyState.tsx +10 -0
  175. package/src/components/DataTable/components/FilterRow.tsx +12 -0
  176. package/src/components/DataTable/components/GroupHeader.tsx +12 -0
  177. package/src/components/DataTable/components/GroupingDropdown.tsx +12 -0
  178. package/src/components/DataTable/components/ImportModal.tsx +7 -0
  179. package/src/components/DataTable/components/LoadingState.tsx +6 -0
  180. package/src/components/DataTable/components/PaginationControls.tsx +16 -1
  181. package/src/components/DataTable/components/RowComponent.tsx +391 -0
  182. package/src/components/DataTable/components/UnifiedTableBody.tsx +63 -851
  183. package/src/components/DataTable/components/VirtualizedDataTable.tsx +16 -4
  184. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +4 -2
  185. package/src/components/DataTable/components/cellValueUtils.ts +40 -0
  186. package/src/components/DataTable/components/hooks/useImportModalFocus.ts +53 -0
  187. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +126 -0
  188. package/src/components/DataTable/context/DataTableContext.tsx +50 -0
  189. package/src/components/DataTable/core/ColumnFactory.ts +31 -0
  190. package/src/components/DataTable/core/DataTableContext.tsx +32 -1
  191. package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +10 -0
  192. package/src/components/DataTable/hooks/useColumnReordering.ts +12 -0
  193. package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +10 -0
  194. package/src/components/DataTable/hooks/useDataTableDataPipeline.ts +16 -0
  195. package/src/components/DataTable/hooks/useDataTablePermissions.ts +127 -33
  196. package/src/components/DataTable/hooks/useDataTableState.ts +35 -1
  197. package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +12 -0
  198. package/src/components/DataTable/hooks/useServerSideDataEffect.ts +11 -0
  199. package/src/components/DataTable/hooks/useTableColumns.ts +8 -0
  200. package/src/components/DataTable/hooks/useTableHandlers.ts +14 -0
  201. package/src/components/DataTable/styles.ts +6 -6
  202. package/src/components/DataTable/types.ts +6 -10
  203. package/src/components/DataTable/utils/a11yUtils.ts +7 -0
  204. package/src/components/DataTable/utils/debugTools.ts +18 -113
  205. package/src/components/DataTable/utils/errorHandling.ts +12 -0
  206. package/src/components/DataTable/utils/exportUtils.ts +9 -0
  207. package/src/components/DataTable/utils/flexibleImport.ts +12 -48
  208. package/src/components/DataTable/utils/paginationUtils.ts +8 -0
  209. package/src/components/DataTable/utils/performanceUtils.ts +5 -1
  210. package/src/components/Dialog/Dialog.tsx +31 -3
  211. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +180 -1
  212. package/src/components/ErrorBoundary/ErrorBoundary.tsx +45 -5
  213. package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +129 -0
  214. package/src/components/ErrorBoundary/index.ts +27 -2
  215. package/src/components/FileDisplay/FileDisplay.tsx +74 -28
  216. package/src/components/FileUpload/FileUpload.tsx +22 -2
  217. package/src/components/Footer/Footer.test.tsx +16 -16
  218. package/src/components/Footer/Footer.tsx +14 -11
  219. package/src/components/Form/Form.tsx +1 -0
  220. package/src/components/Header/Header.test.tsx +43 -73
  221. package/src/components/Header/Header.tsx +59 -49
  222. package/src/components/Input/Input.test.tsx +2 -2
  223. package/src/components/Input/Input.tsx +8 -4
  224. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +4 -4
  225. package/src/components/LoginForm/LoginForm.tsx +4 -0
  226. package/src/components/NavigationMenu/NavigationMenu.tsx +14 -513
  227. package/src/components/NavigationMenu/types.ts +56 -0
  228. package/src/components/NavigationMenu/useNavigationFiltering.ts +390 -0
  229. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
  230. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
  231. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
  232. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +13 -11
  233. package/src/components/PaceAppLayout/PaceAppLayout.tsx +167 -44
  234. package/src/components/PaceAppLayout/README.md +14 -17
  235. package/src/components/PaceAppLayout/test-setup.tsx +3 -4
  236. package/src/components/PaceLoginPage/PaceLoginPage.tsx +3 -0
  237. package/src/components/PasswordChange/PasswordChangeForm.tsx +9 -0
  238. package/src/components/ProtectedRoute/ProtectedRoute.tsx +3 -9
  239. package/src/components/PublicLayout/PublicPageLayout.tsx +2 -5
  240. package/src/components/PublicLayout/PublicPageProvider.tsx +4 -0
  241. package/src/components/Select/Select.tsx +80 -434
  242. package/src/components/Select/context.ts +23 -0
  243. package/src/components/Select/hooks/useSelectEvents.ts +87 -0
  244. package/src/components/Select/hooks/useSelectSearch.ts +91 -0
  245. package/src/components/Select/hooks/useSelectState.ts +104 -0
  246. package/src/components/Select/index.ts +9 -1
  247. package/src/components/Select/types.ts +123 -0
  248. package/src/components/Select/utils/text.ts +26 -0
  249. package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +4 -5
  250. package/src/components/Switch/Switch.tsx +4 -4
  251. package/src/components/Tabs/Tabs.tsx +1 -1
  252. package/src/components/Toast/Toast.tsx +4 -0
  253. package/src/components/Tooltip/Tooltip.tsx +2 -2
  254. package/src/components/UserMenu/UserMenu.test.tsx +24 -11
  255. package/src/components/UserMenu/UserMenu.tsx +21 -18
  256. package/src/components/index.ts +7 -7
  257. package/src/eslint-rules/pace-core-compliance.cjs +106 -0
  258. package/src/hooks/__tests__/index.unit.test.ts +2 -5
  259. package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
  260. package/src/hooks/index.ts +1 -2
  261. package/src/hooks/public/usePublicEvent.ts +4 -0
  262. package/src/hooks/public/usePublicEventLogo.ts +4 -0
  263. package/src/hooks/public/usePublicFileDisplay.ts +4 -0
  264. package/src/hooks/public/usePublicRouteParams.ts +4 -0
  265. package/src/hooks/services/useAuth.ts +32 -0
  266. package/src/hooks/services/useCurrentEvent.ts +6 -0
  267. package/src/hooks/services/useCurrentOrganisation.ts +6 -0
  268. package/src/hooks/useAppConfig.ts +15 -30
  269. package/src/hooks/useDebounce.ts +9 -0
  270. package/src/hooks/useEventTheme.ts +6 -0
  271. package/src/hooks/useFileDisplay.ts +81 -50
  272. package/src/hooks/useFileReference.ts +25 -7
  273. package/src/hooks/useFileUrl.ts +11 -1
  274. package/src/hooks/useFocusManagement.ts +14 -0
  275. package/src/hooks/useFocusTrap.ts +3 -0
  276. package/src/hooks/useInactivityTracker.ts +3 -0
  277. package/src/hooks/useKeyboardShortcuts.ts +4 -0
  278. package/src/hooks/useOrganisationPermissions.ts +4 -0
  279. package/src/hooks/useOrganisationSecurity.ts +4 -0
  280. package/src/hooks/usePerformanceMonitor.ts +4 -0
  281. package/src/hooks/usePermissionCache.ts +7 -0
  282. package/src/hooks/useQueryCache.ts +12 -1
  283. package/src/hooks/useSessionRestoration.ts +4 -0
  284. package/src/hooks/useStorage.ts +4 -0
  285. package/src/hooks/useToast.ts +1 -1
  286. package/src/index.ts +6 -6
  287. package/src/providers/__tests__/OrganisationProvider.test.tsx +92 -70
  288. package/src/providers/services/AuthServiceProvider.tsx +35 -7
  289. package/src/providers/services/EventServiceProvider.tsx +51 -5
  290. package/src/providers/services/InactivityServiceProvider.tsx +18 -0
  291. package/src/providers/services/OrganisationServiceProvider.tsx +18 -0
  292. package/src/providers/services/UnifiedAuthProvider.tsx +126 -134
  293. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +29 -13
  294. package/src/rbac/README.md +1 -1
  295. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
  296. package/src/rbac/__tests__/scenarios.user-role.test.tsx +4 -5
  297. package/src/rbac/adapters.tsx +12 -3
  298. package/src/rbac/api.test.ts +59 -51
  299. package/src/rbac/api.ts +246 -167
  300. package/src/rbac/components/NavigationProvider.tsx +4 -1
  301. package/src/rbac/components/PagePermissionGuard.tsx +185 -17
  302. package/src/rbac/components/RoleBasedRouter.tsx +5 -1
  303. package/src/rbac/components/SecureDataProvider.test.tsx +84 -49
  304. package/src/rbac/components/SecureDataProvider.tsx +20 -5
  305. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +24 -14
  306. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +7 -0
  307. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +14 -6
  308. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +15 -4
  309. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +148 -24
  310. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +81 -15
  311. package/src/rbac/engine.ts +38 -14
  312. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
  313. package/src/rbac/hooks/permissions/index.ts +7 -0
  314. package/src/rbac/hooks/permissions/useAccessLevel.ts +105 -0
  315. package/src/rbac/hooks/permissions/useCachedPermissions.ts +79 -0
  316. package/src/rbac/hooks/permissions/useCan.ts +377 -0
  317. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +90 -0
  318. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +90 -0
  319. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +93 -0
  320. package/src/rbac/hooks/permissions/usePermissions.ts +253 -0
  321. package/src/rbac/hooks/useCan.test.ts +64 -66
  322. package/src/rbac/hooks/usePermissions.ts +14 -995
  323. package/src/rbac/hooks/useRBAC.test.ts +1 -5
  324. package/src/rbac/hooks/useRBAC.ts +36 -37
  325. package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
  326. package/src/rbac/hooks/useResolvedScope.ts +35 -40
  327. package/src/rbac/hooks/useResourcePermissions.test.ts +54 -18
  328. package/src/rbac/hooks/useResourcePermissions.ts +14 -4
  329. package/src/rbac/hooks/useSecureSupabase.ts +27 -7
  330. package/src/rbac/index.ts +7 -0
  331. package/src/rbac/permissions.ts +0 -30
  332. package/src/rbac/secureClient.test.ts +22 -18
  333. package/src/rbac/secureClient.ts +294 -68
  334. package/src/rbac/security.ts +0 -17
  335. package/src/rbac/types.ts +9 -0
  336. package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
  337. package/src/rbac/utils/clientSecurity.ts +93 -0
  338. package/src/rbac/utils/contextValidator.ts +77 -168
  339. package/src/services/AuthService.ts +39 -7
  340. package/src/services/EventService.ts +186 -54
  341. package/src/services/OrganisationService.ts +81 -14
  342. package/src/services/__tests__/EventService.test.ts +1 -2
  343. package/src/services/base/BaseService.ts +3 -0
  344. package/src/theming/__tests__/parseEventColours.test.ts +6 -9
  345. package/src/theming/parseEventColours.ts +5 -19
  346. package/src/types/vitest-globals.d.ts +51 -26
  347. package/src/utils/__mocks__/supabaseMock.ts +1 -3
  348. package/src/utils/__tests__/formatting.unit.test.ts +4 -4
  349. package/src/utils/__tests__/index.unit.test.ts +2 -2
  350. package/src/utils/audit/audit.ts +0 -3
  351. package/src/utils/core/cn.ts +1 -1
  352. package/src/utils/dynamic/dynamicUtils.ts +7 -4
  353. package/src/utils/file-reference/index.ts +53 -1
  354. package/src/utils/formatting/formatting.ts +8 -18
  355. package/src/utils/index.ts +0 -1
  356. package/dist/chunk-3QRJFVBR.js.map +0 -1
  357. package/dist/chunk-3XTALGJF.js.map +0 -1
  358. package/dist/chunk-4N5C5XZU.js.map +0 -1
  359. package/dist/chunk-4ZC4GX36.js.map +0 -1
  360. package/dist/chunk-7D4SUZUM.js +0 -38
  361. package/dist/chunk-BYFSK72L.js.map +0 -1
  362. package/dist/chunk-EXUD6RNJ.js +0 -451
  363. package/dist/chunk-EXUD6RNJ.js.map +0 -1
  364. package/dist/chunk-GLK6VM3F.js.map +0 -1
  365. package/dist/chunk-I7PSE6JW.js.map +0 -1
  366. package/dist/chunk-JBKQ3SAO.js.map +0 -1
  367. package/dist/chunk-KNC55RTG.js.map +0 -1
  368. package/dist/chunk-LXQLPRQ2.js.map +0 -1
  369. package/dist/chunk-R77UEZ4E.js.map +0 -1
  370. package/dist/chunk-SQGMNID3.js.map +0 -1
  371. package/dist/chunk-T33XF5ZC.js +0 -12922
  372. package/dist/chunk-T33XF5ZC.js.map +0 -1
  373. package/dist/chunk-XM25TVIE.js.map +0 -1
  374. package/docs/api/classes/ColumnFactory.md +0 -243
  375. package/docs/api/classes/ErrorBoundary.md +0 -144
  376. package/docs/api/classes/InvalidScopeError.md +0 -73
  377. package/docs/api/classes/Logger.md +0 -178
  378. package/docs/api/classes/MissingUserContextError.md +0 -66
  379. package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
  380. package/docs/api/classes/PermissionDeniedError.md +0 -73
  381. package/docs/api/classes/RBACAuditManager.md +0 -297
  382. package/docs/api/classes/RBACCache.md +0 -322
  383. package/docs/api/classes/RBACEngine.md +0 -171
  384. package/docs/api/classes/RBACError.md +0 -76
  385. package/docs/api/classes/RBACNotInitializedError.md +0 -66
  386. package/docs/api/classes/SecureSupabaseClient.md +0 -160
  387. package/docs/api/classes/StorageUtils.md +0 -328
  388. package/docs/api/enums/FileCategory.md +0 -184
  389. package/docs/api/enums/LogLevel.md +0 -54
  390. package/docs/api/enums/RBACErrorCode.md +0 -228
  391. package/docs/api/enums/RPCFunction.md +0 -118
  392. package/docs/api/interfaces/AddressFieldProps.md +0 -241
  393. package/docs/api/interfaces/AddressFieldRef.md +0 -94
  394. package/docs/api/interfaces/AggregateConfig.md +0 -43
  395. package/docs/api/interfaces/AutocompleteOptions.md +0 -75
  396. package/docs/api/interfaces/AvatarProps.md +0 -128
  397. package/docs/api/interfaces/BadgeProps.md +0 -27
  398. package/docs/api/interfaces/ButtonProps.md +0 -53
  399. package/docs/api/interfaces/CalendarProps.md +0 -70
  400. package/docs/api/interfaces/CardProps.md +0 -66
  401. package/docs/api/interfaces/ColorPalette.md +0 -7
  402. package/docs/api/interfaces/ColorShade.md +0 -66
  403. package/docs/api/interfaces/ComplianceResult.md +0 -30
  404. package/docs/api/interfaces/DataAccessRecord.md +0 -96
  405. package/docs/api/interfaces/DataRecord.md +0 -11
  406. package/docs/api/interfaces/DataTableAction.md +0 -249
  407. package/docs/api/interfaces/DataTableColumn.md +0 -504
  408. package/docs/api/interfaces/DataTableProps.md +0 -625
  409. package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
  410. package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
  411. package/docs/api/interfaces/DatabaseIssue.md +0 -41
  412. package/docs/api/interfaces/EmptyStateConfig.md +0 -61
  413. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
  414. package/docs/api/interfaces/EventAppRoleData.md +0 -71
  415. package/docs/api/interfaces/ExportColumn.md +0 -90
  416. package/docs/api/interfaces/ExportOptions.md +0 -126
  417. package/docs/api/interfaces/FileDisplayProps.md +0 -249
  418. package/docs/api/interfaces/FileMetadata.md +0 -129
  419. package/docs/api/interfaces/FileReference.md +0 -118
  420. package/docs/api/interfaces/FileSizeLimits.md +0 -7
  421. package/docs/api/interfaces/FileUploadOptions.md +0 -139
  422. package/docs/api/interfaces/FileUploadProps.md +0 -293
  423. package/docs/api/interfaces/FooterProps.md +0 -105
  424. package/docs/api/interfaces/FormFieldProps.md +0 -166
  425. package/docs/api/interfaces/FormProps.md +0 -113
  426. package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
  427. package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
  428. package/docs/api/interfaces/InputProps.md +0 -53
  429. package/docs/api/interfaces/LabelProps.md +0 -107
  430. package/docs/api/interfaces/LoggerConfig.md +0 -62
  431. package/docs/api/interfaces/LoginFormProps.md +0 -184
  432. package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
  433. package/docs/api/interfaces/NavigationContextType.md +0 -164
  434. package/docs/api/interfaces/NavigationGuardProps.md +0 -139
  435. package/docs/api/interfaces/NavigationItem.md +0 -120
  436. package/docs/api/interfaces/NavigationMenuProps.md +0 -221
  437. package/docs/api/interfaces/NavigationProviderProps.md +0 -117
  438. package/docs/api/interfaces/Organisation.md +0 -140
  439. package/docs/api/interfaces/OrganisationContextType.md +0 -388
  440. package/docs/api/interfaces/OrganisationMembership.md +0 -140
  441. package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
  442. package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
  443. package/docs/api/interfaces/PaceAppLayoutProps.md +0 -406
  444. package/docs/api/interfaces/PaceLoginPageProps.md +0 -47
  445. package/docs/api/interfaces/PageAccessRecord.md +0 -85
  446. package/docs/api/interfaces/PagePermissionContextType.md +0 -140
  447. package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
  448. package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
  449. package/docs/api/interfaces/PaletteData.md +0 -41
  450. package/docs/api/interfaces/ParsedAddress.md +0 -120
  451. package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
  452. package/docs/api/interfaces/ProgressProps.md +0 -42
  453. package/docs/api/interfaces/ProtectedRouteProps.md +0 -97
  454. package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
  455. package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
  456. package/docs/api/interfaces/PublicPageLayoutProps.md +0 -198
  457. package/docs/api/interfaces/QuickFix.md +0 -52
  458. package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
  459. package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
  460. package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
  461. package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
  462. package/docs/api/interfaces/RBACConfig.md +0 -133
  463. package/docs/api/interfaces/RBACContext.md +0 -52
  464. package/docs/api/interfaces/RBACLogger.md +0 -112
  465. package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
  466. package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
  467. package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
  468. package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
  469. package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
  470. package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
  471. package/docs/api/interfaces/RBACResult.md +0 -58
  472. package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
  473. package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
  474. package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
  475. package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
  476. package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
  477. package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
  478. package/docs/api/interfaces/RBACRolesListParams.md +0 -52
  479. package/docs/api/interfaces/RBACRolesListResult.md +0 -74
  480. package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
  481. package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
  482. package/docs/api/interfaces/ResourcePermissions.md +0 -155
  483. package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
  484. package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
  485. package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
  486. package/docs/api/interfaces/RoleManagementResult.md +0 -52
  487. package/docs/api/interfaces/RouteAccessRecord.md +0 -107
  488. package/docs/api/interfaces/RouteConfig.md +0 -134
  489. package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
  490. package/docs/api/interfaces/SecureDataContextType.md +0 -168
  491. package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
  492. package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
  493. package/docs/api/interfaces/SetupIssue.md +0 -41
  494. package/docs/api/interfaces/StorageConfig.md +0 -41
  495. package/docs/api/interfaces/StorageFileInfo.md +0 -74
  496. package/docs/api/interfaces/StorageFileMetadata.md +0 -151
  497. package/docs/api/interfaces/StorageListOptions.md +0 -99
  498. package/docs/api/interfaces/StorageListResult.md +0 -41
  499. package/docs/api/interfaces/StorageUploadOptions.md +0 -101
  500. package/docs/api/interfaces/StorageUploadResult.md +0 -63
  501. package/docs/api/interfaces/StorageUrlOptions.md +0 -60
  502. package/docs/api/interfaces/StyleImport.md +0 -19
  503. package/docs/api/interfaces/SwitchProps.md +0 -34
  504. package/docs/api/interfaces/TabsContentProps.md +0 -9
  505. package/docs/api/interfaces/TabsListProps.md +0 -9
  506. package/docs/api/interfaces/TabsProps.md +0 -9
  507. package/docs/api/interfaces/TabsTriggerProps.md +0 -50
  508. package/docs/api/interfaces/TextareaProps.md +0 -53
  509. package/docs/api/interfaces/ToastActionElement.md +0 -9
  510. package/docs/api/interfaces/ToastProps.md +0 -9
  511. package/docs/api/interfaces/UnifiedAuthContextType.md +0 -820
  512. package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -171
  513. package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
  514. package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
  515. package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -136
  516. package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
  517. package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
  518. package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -81
  519. package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
  520. package/docs/api/interfaces/UsePublicEventReturn.md +0 -68
  521. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
  522. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -120
  523. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -94
  524. package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
  525. package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
  526. package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
  527. package/docs/api/interfaces/UserEventAccess.md +0 -118
  528. package/docs/api/interfaces/UserMenuProps.md +0 -86
  529. package/docs/api/interfaces/UserProfile.md +0 -63
  530. package/docs/migration/quick-migration-guide.md +0 -356
  531. package/docs/migration/service-architecture.md +0 -281
  532. package/src/components/EventSelector/EventSelector.test.tsx +0 -720
  533. package/src/components/EventSelector/EventSelector.tsx +0 -420
  534. package/src/components/EventSelector/index.ts +0 -3
  535. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
  536. package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -324
  537. package/src/components/OrganisationSelector/index.ts +0 -9
  538. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +0 -680
  539. package/src/hooks/useSecureDataAccess.test.ts +0 -559
  540. package/src/hooks/useSecureDataAccess.ts +0 -681
  541. /package/dist/{DataTable-DQ7RSOHE.js.map → DataTable-THFPBKTP.js.map} +0 -0
  542. /package/dist/{UnifiedAuthProvider-ATAP5UTR.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
  543. /package/dist/{api-N774RPUA.js.map → api-IAGWF3ZG.js.map} +0 -0
  544. /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
  545. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  546. /package/docs/migration/{organisation-context-timing-fix.md → V0.3.44_organisation-context-timing-fix.md} +0 -0
  547. /package/docs/migration/{rbac-migration.md → V0.4.0_rbac-migration.md} +0 -0
  548. /package/docs/migration/{person-scoped-profiles-migration-guide.md → V0.5.190_person-scoped-profiles-migration-guide.md} +0 -0
  549. /package/docs/migration/{REACT_19_MIGRATION.md → V0.6.0_REACT_19_MIGRATION.md} +0 -0
@@ -0,0 +1,377 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+
3
+ import { isPermitted, isPermittedCached } from '../../api';
4
+ import { getRBACLogger } from '../../config';
5
+ import { Permission, Scope, UUID } from '../../types';
6
+ import { scopeEqual } from '../../utils/deep-equal';
7
+
8
+ /**
9
+ * Hook to check if user can perform an action
10
+ *
11
+ * @param userId - User ID
12
+ * @param scope - Scope for permission checking
13
+ * @param permission - Permission to check
14
+ * @param pageId - Optional page ID
15
+ * @param useCache - Whether to use cached results
16
+ * @param appName - Optional app name (for PORTAL/ADMIN special case)
17
+ * @returns Permission check state and methods
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * function MyComponent() {
22
+ * const { can, isLoading, error } = useCan(userId, scope, 'read:users');
23
+ *
24
+ * if (isLoading) return <div>Checking permission...</div>;
25
+ * if (error) return <div>Error: {error.message}</div>;
26
+ *
27
+ * return can ? <UserList /> : <div>Access denied</div>;
28
+ * }
29
+ * ```
30
+ */
31
+ export function useCan(
32
+ userId: UUID,
33
+ scope: Scope,
34
+ permission: Permission,
35
+ pageId?: UUID,
36
+ useCache: boolean = true,
37
+ /**
38
+ * Pre-computed super admin flag to avoid duplicate super admin checks.
39
+ * Callers should check super admin once and pass the result to all useCan hooks.
40
+ * Pass null if not checked yet, false/true if checked.
41
+ * Defaults to null (not checked yet) - hook will check if needed.
42
+ */
43
+ precomputedSuperAdmin: boolean | null = null,
44
+ appName?: string,
45
+ ) {
46
+ // CRITICAL FIX: Initialize isSuperAdmin from precomputed value immediately
47
+ // This prevents permission checks from running when we already know the user is a super admin
48
+ // If precomputedSuperAdmin is true, we can immediately set can=true and isLoading=false
49
+ const [isSuperAdmin, setIsSuperAdmin] = useState<boolean | null>(precomputedSuperAdmin ?? null);
50
+
51
+ // For super admins, immediately grant permissions without waiting
52
+ const initialCan = precomputedSuperAdmin === true ? true : false;
53
+ const initialIsLoading = precomputedSuperAdmin === true ? false : true;
54
+
55
+ const [can, setCan] = useState<boolean>(initialCan);
56
+ const [isLoading, setIsLoading] = useState<boolean>(initialIsLoading);
57
+ const [error, setError] = useState<Error | null>(null);
58
+
59
+ // Validate scope parameter - handle undefined/null scope gracefully
60
+ const isValidScope = scope && typeof scope === 'object';
61
+ const organisationId = isValidScope ? scope.organisationId : undefined;
62
+ const eventId = isValidScope ? scope.eventId : undefined;
63
+ const appId = isValidScope ? scope.appId : undefined;
64
+
65
+ // CRITICAL FIX: Immediately update state when precomputedSuperAdmin changes to true
66
+ // This ensures super admins get immediate access without waiting for permission checks
67
+ useEffect(() => {
68
+ if (precomputedSuperAdmin === true && isSuperAdmin !== true) {
69
+ setIsSuperAdmin(true);
70
+ setCan(true);
71
+ setIsLoading(false);
72
+ setError(null);
73
+ } else if (precomputedSuperAdmin === false && isSuperAdmin !== false) {
74
+ setIsSuperAdmin(false);
75
+ }
76
+ }, [precomputedSuperAdmin, isSuperAdmin]);
77
+
78
+ // Check super-admin status - super admins bypass organisation context requirements
79
+ // PERFORMANCE OPTIMIZATION: Use precomputed value directly - no duplicate checks
80
+ // Callers must check super admin once and pass the result (null if not checked yet)
81
+ useEffect(() => {
82
+ // If precomputed value is null, it means not checked yet - check ourselves
83
+ if (precomputedSuperAdmin === null) {
84
+ if (!userId) {
85
+ setIsSuperAdmin(false);
86
+ return;
87
+ }
88
+
89
+ let cancelled = false;
90
+ const checkSuperAdmin = async () => {
91
+ const startTime = Date.now();
92
+ try {
93
+ const { isSuperAdmin: checkSuperAdmin } = await import('../../api');
94
+
95
+ // Add timeout warning
96
+ const timeoutWarning = setTimeout(() => {
97
+ if (!cancelled) {
98
+ console.warn('[useCan] Super admin check taking longer than 5 seconds', {
99
+ userId,
100
+ elapsedMs: Date.now() - startTime,
101
+ });
102
+ }
103
+ }, 5000);
104
+
105
+ const isSuper = await checkSuperAdmin(userId);
106
+ clearTimeout(timeoutWarning);
107
+
108
+ if (!cancelled) {
109
+ const elapsed = Date.now() - startTime;
110
+ if (elapsed > 1000) {
111
+ console.warn('[useCan] Super admin check took longer than expected', {
112
+ userId,
113
+ elapsedMs: elapsed,
114
+ });
115
+ }
116
+ setIsSuperAdmin(isSuper);
117
+ // If super admin, immediately grant permissions
118
+ if (isSuper) {
119
+ setCan(true);
120
+ setIsLoading(false);
121
+ setError(null);
122
+ }
123
+ }
124
+ } catch (err) {
125
+ if (!cancelled) {
126
+ const elapsed = Date.now() - startTime;
127
+ console.error('[useCan] Error checking super admin', {
128
+ userId,
129
+ error: err,
130
+ elapsedMs: elapsed,
131
+ });
132
+ setIsSuperAdmin(false);
133
+ }
134
+ }
135
+ };
136
+
137
+ checkSuperAdmin();
138
+ return () => {
139
+ cancelled = true;
140
+ };
141
+ }
142
+ }, [userId, precomputedSuperAdmin]);
143
+
144
+ // Add timeout for missing organisation context (3 seconds)
145
+ // Only apply timeout for resource-level permissions, not page-level (which can handle null orgs)
146
+ // Super admins bypass this check
147
+ useEffect(() => {
148
+ const isPagePermission = permission.includes(':page.') || !!pageId;
149
+ const requiresOrgId = !isPagePermission;
150
+
151
+ // Don't block if user is super-admin (they bypass context requirements)
152
+ if (isSuperAdmin === true) {
153
+ return;
154
+ }
155
+
156
+ if (requiresOrgId && (!isValidScope || !organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
157
+ const timeoutId = setTimeout(() => {
158
+ setError(new Error('Organisation context is required for permission checks'));
159
+ setIsLoading(false);
160
+ setCan(false);
161
+ }, 3000); // 3 seconds - typical permission check is < 1 second
162
+
163
+ return () => clearTimeout(timeoutId);
164
+ }
165
+ // Clear error if organisation context becomes available
166
+ if (error?.message === 'Organisation context is required for permission checks') {
167
+ setError(null);
168
+ }
169
+ }, [isValidScope, organisationId, error, permission, pageId, isSuperAdmin]);
170
+
171
+ // Use refs to track the last values to prevent unnecessary re-runs
172
+ const lastUserIdRef = useRef<UUID | null>(null);
173
+ const lastScopeRef = useRef<string | null>(null);
174
+ const lastPermissionRef = useRef<Permission | null>(null);
175
+ const lastPageIdRef = useRef<UUID | undefined | null>(null);
176
+ const lastUseCacheRef = useRef<boolean | null>(null);
177
+ const lastIsSuperAdminRef = useRef<boolean | null>(null);
178
+
179
+ // Create a stable scope object for comparison
180
+ const stableScope = useMemo(() => {
181
+ if (!isValidScope) {
182
+ return null;
183
+ }
184
+ return {
185
+ organisationId,
186
+ eventId,
187
+ appId,
188
+ };
189
+ }, [isValidScope, organisationId, eventId, appId]);
190
+
191
+ // Track previous scope for deep equality comparison
192
+ const prevScopeRef = useRef<Scope | null>(null);
193
+
194
+ useEffect(() => {
195
+ // Use deep equality check for scope to prevent unnecessary re-runs
196
+ const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
197
+
198
+ // Only run if something has actually changed
199
+ // CRITICAL: Also check if isSuperAdmin changed - super admins bypass all checks
200
+ const isSuperAdminChanged = lastIsSuperAdminRef.current !== isSuperAdmin;
201
+
202
+ if (
203
+ lastUserIdRef.current !== userId ||
204
+ scopeChanged ||
205
+ lastPermissionRef.current !== permission ||
206
+ lastPageIdRef.current !== pageId ||
207
+ lastUseCacheRef.current !== useCache ||
208
+ isSuperAdminChanged
209
+ ) {
210
+ lastIsSuperAdminRef.current = isSuperAdmin;
211
+ lastUserIdRef.current = userId;
212
+ prevScopeRef.current = stableScope;
213
+ lastPermissionRef.current = permission;
214
+ lastPageIdRef.current = pageId;
215
+ lastUseCacheRef.current = useCache;
216
+
217
+ // Inline the permission check logic to avoid useCallback dependency issues
218
+ const checkPermission = async () => {
219
+ if (!userId) {
220
+ setCan(false);
221
+ setIsLoading(false);
222
+ return;
223
+ }
224
+
225
+ // CRITICAL: Super admins bypass all permission checks - grant immediately
226
+ // This must be checked BEFORE any other validation to avoid unnecessary API calls
227
+ if (isSuperAdmin === true) {
228
+ setCan(true);
229
+ setIsLoading(false);
230
+ setError(null);
231
+ return;
232
+ }
233
+
234
+ // If super admin status is still being checked (null), wait for it to complete
235
+ // Don't proceed with permission check until we know if user is super admin
236
+ if (isSuperAdmin === null) {
237
+ setIsLoading(true);
238
+ setCan(false);
239
+ setError(null);
240
+ return;
241
+ }
242
+
243
+ // Validate scope before accessing properties
244
+ if (!isValidScope) {
245
+ setIsLoading(true);
246
+ setCan(false);
247
+ setError(null);
248
+ // Timeout is handled in separate useEffect
249
+ return;
250
+ }
251
+
252
+ // For page-level permissions, allow undefined/null organisationId (database function handles it)
253
+ // For resource-level permissions, organisationId is required
254
+ const isPagePermission = permission.includes(':page.') || !!pageId;
255
+ const requiresOrgId = !isPagePermission;
256
+
257
+ // Check if pageId is a pageName (not a UUID) - if so, we need appId to resolve it
258
+ const isPageName = pageId && typeof pageId === 'string' && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(pageId);
259
+ const needsAppIdForPageName = isPagePermission && isPageName;
260
+
261
+ // Don't check permissions if scope is invalid and orgId is required
262
+ // Wait for organisation context to resolve (unless it's a page permission that can handle null orgs)
263
+ if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
264
+ // Not super-admin (already checked above) - wait for org context
265
+ setIsLoading(true);
266
+ setCan(false);
267
+ setError(null);
268
+ // Timeout is handled in separate useEffect (Phase 1.4)
269
+ return;
270
+ }
271
+
272
+ // For page-level permissions with pageName (not UUID), we need appId to resolve the pageName to pageId
273
+ // Wait for appId to be available before checking permissions
274
+ if (needsAppIdForPageName && (!appId || appId === null || (typeof appId === 'string' && appId.trim() === ''))) {
275
+ setIsLoading(true);
276
+ setCan(false);
277
+ setError(null);
278
+ // Will re-run when appId becomes available (via scope change detection)
279
+ return;
280
+ }
281
+
282
+ try {
283
+ setIsLoading(true);
284
+ setError(null);
285
+
286
+ // Create a valid scope object for the API call
287
+ // For page-level permissions, organisationId can be undefined (database handles it)
288
+ const validScope: Scope = {
289
+ ...(organisationId ? { organisationId } : {}),
290
+ ...(eventId ? { eventId } : {}),
291
+ ...(appId ? { appId } : {})
292
+ };
293
+
294
+ // Pass super admin status to avoid duplicate check in isPermitted
295
+ // Note: isPermittedCached doesn't support precomputedSuperAdmin, but the check will be cached
296
+ // If we know user is NOT super admin (isSuperAdmin === false), pass false to skip the check
297
+ const result = useCache
298
+ ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
299
+ : await isPermitted({ userId, scope: validScope, permission, pageId }, appName, isSuperAdmin === false ? false : null);
300
+
301
+ setCan(result);
302
+ } catch (err) {
303
+ const logger = getRBACLogger();
304
+ logger.error('Permission check error:', { permission, error: err });
305
+ console.error('[useCan] Permission check error', { userId, permission, error: err });
306
+ setError(err instanceof Error ? err : new Error('Failed to check permission'));
307
+ setCan(false);
308
+ } finally {
309
+ setIsLoading(false);
310
+ }
311
+ };
312
+
313
+ checkPermission();
314
+ }
315
+ }, [userId, stableScope, permission, pageId, useCache, appName, isSuperAdmin]);
316
+
317
+ const refetch = useCallback(async () => {
318
+ if (!userId) {
319
+ setCan(false);
320
+ setIsLoading(false);
321
+ return;
322
+ }
323
+
324
+ // Validate scope before accessing properties
325
+ if (!isValidScope) {
326
+ setCan(false);
327
+ setIsLoading(true);
328
+ setError(null);
329
+ return;
330
+ }
331
+
332
+ // For page-level permissions, allow undefined/null organisationId (database function handles it)
333
+ // For resource-level permissions, organisationId is required
334
+ const isPagePermission = permission.includes(':page.') || !!pageId;
335
+ const requiresOrgId = !isPagePermission;
336
+
337
+ // Don't check permissions if scope is invalid and orgId is required
338
+ if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
339
+ setCan(false);
340
+ setIsLoading(true);
341
+ setError(null);
342
+ return;
343
+ }
344
+
345
+ try {
346
+ setIsLoading(true);
347
+ setError(null);
348
+
349
+ // Create a valid scope object for the API call
350
+ // For page-level permissions, organisationId can be undefined (database handles it)
351
+ const validScope: Scope = {
352
+ ...(organisationId ? { organisationId } : {}),
353
+ ...(eventId ? { eventId } : {}),
354
+ ...(appId ? { appId } : {})
355
+ };
356
+
357
+ const result = useCache
358
+ ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
359
+ : await isPermitted({ userId, scope: validScope, permission, pageId }, appName, null);
360
+
361
+ setCan(result);
362
+ } catch (err) {
363
+ setError(err instanceof Error ? err : new Error('Failed to check permission'));
364
+ setCan(false);
365
+ } finally {
366
+ setIsLoading(false);
367
+ }
368
+ }, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache, appName]);
369
+
370
+ // Memoize the return object to prevent unnecessary re-renders
371
+ return useMemo(() => ({
372
+ can,
373
+ isLoading,
374
+ error,
375
+ refetch
376
+ }), [can, isLoading, error, refetch]);
377
+ }
@@ -0,0 +1,90 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ import { isPermitted, isPermittedCached } from '../../api';
4
+ import { Permission, Scope, UUID } from '../../types';
5
+
6
+ /**
7
+ * Hook to check if user has all of the specified permissions
8
+ *
9
+ * @param userId - User ID
10
+ * @param scope - Scope for permission checking
11
+ * @param permissions - Array of permissions to check
12
+ * @param useCache - Whether to use cached results
13
+ * @returns Whether user has all of the permissions
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * function MyComponent() {
18
+ * const { hasAll, isLoading, error } = useHasAllPermissions(
19
+ * userId,
20
+ * scope,
21
+ * ['read:users', 'create:users', 'update:users']
22
+ * );
23
+ *
24
+ * if (isLoading) return <div>Checking permissions...</div>;
25
+ * if (error) return <div>Error: {error.message}</div>;
26
+ *
27
+ * return hasAll ? <FullUserManagementPanel /> : <div>Insufficient permissions</div>;
28
+ * }
29
+ * ```
30
+ */
31
+ export function useHasAllPermissions(
32
+ userId: UUID,
33
+ scope: Scope,
34
+ permissions: Permission[],
35
+ useCache: boolean = true
36
+ ): {
37
+ hasAll: boolean;
38
+ isLoading: boolean;
39
+ error: Error | null;
40
+ refetch: () => Promise<void>;
41
+ } {
42
+ const [hasAll, setHasAll] = useState<boolean>(false);
43
+ const [isLoading, setIsLoading] = useState(true);
44
+ const [error, setError] = useState<Error | null>(null);
45
+
46
+ const checkAllPermissions = useCallback(async () => {
47
+ if (!userId || permissions.length === 0) {
48
+ setHasAll(false);
49
+ setIsLoading(false);
50
+ return;
51
+ }
52
+
53
+ try {
54
+ setIsLoading(true);
55
+ setError(null);
56
+
57
+ let hasAllPermissions = true;
58
+
59
+ for (const permission of permissions) {
60
+ const result = useCache
61
+ ? await isPermittedCached({ userId, scope, permission })
62
+ : await isPermitted({ userId, scope, permission });
63
+
64
+ if (!result) {
65
+ hasAllPermissions = false;
66
+ break;
67
+ }
68
+ }
69
+
70
+ setHasAll(hasAllPermissions);
71
+ } catch (err) {
72
+ setError(err instanceof Error ? err : new Error('Failed to check permissions'));
73
+ setHasAll(false);
74
+ } finally {
75
+ setIsLoading(false);
76
+ }
77
+ }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
78
+
79
+ useEffect(() => {
80
+ checkAllPermissions();
81
+ }, [checkAllPermissions]);
82
+
83
+ // Memoize the return object to prevent unnecessary re-renders
84
+ return useMemo(() => ({
85
+ hasAll,
86
+ isLoading,
87
+ error,
88
+ refetch: checkAllPermissions
89
+ }), [hasAll, isLoading, error, checkAllPermissions]);
90
+ }
@@ -0,0 +1,90 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ import { isPermitted, isPermittedCached } from '../../api';
4
+ import { Permission, Scope, UUID } from '../../types';
5
+
6
+ /**
7
+ * Hook to check if user has any of the specified permissions
8
+ *
9
+ * @param userId - User ID
10
+ * @param scope - Scope for permission checking
11
+ * @param permissions - Array of permissions to check
12
+ * @param useCache - Whether to use cached results
13
+ * @returns Whether user has any of the permissions
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * function MyComponent() {
18
+ * const { hasAny, isLoading, error } = useHasAnyPermission(
19
+ * userId,
20
+ * scope,
21
+ * ['read:users', 'create:users']
22
+ * );
23
+ *
24
+ * if (isLoading) return <div>Checking permissions...</div>;
25
+ * if (error) return <div>Error: {error.message}</div>;
26
+ *
27
+ * return hasAny ? <UserManagementPanel /> : <div>No user permissions</div>;
28
+ * }
29
+ * ```
30
+ */
31
+ export function useHasAnyPermission(
32
+ userId: UUID,
33
+ scope: Scope,
34
+ permissions: Permission[],
35
+ useCache: boolean = true
36
+ ): {
37
+ hasAny: boolean;
38
+ isLoading: boolean;
39
+ error: Error | null;
40
+ refetch: () => Promise<void>;
41
+ } {
42
+ const [hasAny, setHasAny] = useState<boolean>(false);
43
+ const [isLoading, setIsLoading] = useState(true);
44
+ const [error, setError] = useState<Error | null>(null);
45
+
46
+ const checkAnyPermission = useCallback(async () => {
47
+ if (!userId || permissions.length === 0) {
48
+ setHasAny(false);
49
+ setIsLoading(false);
50
+ return;
51
+ }
52
+
53
+ try {
54
+ setIsLoading(true);
55
+ setError(null);
56
+
57
+ let hasAnyPermission = false;
58
+
59
+ for (const permission of permissions) {
60
+ const result = useCache
61
+ ? await isPermittedCached({ userId, scope, permission })
62
+ : await isPermitted({ userId, scope, permission });
63
+
64
+ if (result) {
65
+ hasAnyPermission = true;
66
+ break;
67
+ }
68
+ }
69
+
70
+ setHasAny(hasAnyPermission);
71
+ } catch (err) {
72
+ setError(err instanceof Error ? err : new Error('Failed to check permissions'));
73
+ setHasAny(false);
74
+ } finally {
75
+ setIsLoading(false);
76
+ }
77
+ }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
78
+
79
+ useEffect(() => {
80
+ checkAnyPermission();
81
+ }, [checkAnyPermission]);
82
+
83
+ // Memoize the return object to prevent unnecessary re-renders
84
+ return useMemo(() => ({
85
+ hasAny,
86
+ isLoading,
87
+ error,
88
+ refetch: checkAnyPermission
89
+ }), [hasAny, isLoading, error, checkAnyPermission]);
90
+ }
@@ -0,0 +1,93 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ import { isPermitted, isPermittedCached } from '../../api';
4
+ import { Permission, Scope, UUID } from '../../types';
5
+
6
+ /**
7
+ * Hook to check multiple permissions at once
8
+ *
9
+ * @param userId - User ID
10
+ * @param scope - Scope for permission checking
11
+ * @param permissions - Array of permissions to check
12
+ * @param useCache - Whether to use cached results
13
+ * @returns Multiple permission check results
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * function MyComponent() {
18
+ * const { results, isLoading, error } = useMultiplePermissions(
19
+ * userId,
20
+ * scope,
21
+ * ['read:users', 'create:users', 'update:users']
22
+ * );
23
+ *
24
+ * if (isLoading) return <div>Checking permissions...</div>;
25
+ * if (error) return <div>Error: {error.message}</div>;
26
+ *
27
+ * return (
28
+ * <div>
29
+ * {results['read:users'] && <UserList />}
30
+ * {results['create:users'] && <CreateUserButton />}
31
+ * {results['update:users'] && <EditUserButton />}
32
+ * </div>
33
+ * );
34
+ * }
35
+ * ```
36
+ */
37
+ export function useMultiplePermissions(
38
+ userId: UUID,
39
+ scope: Scope,
40
+ permissions: Permission[],
41
+ useCache: boolean = true
42
+ ): {
43
+ results: Record<Permission, boolean>;
44
+ isLoading: boolean;
45
+ error: Error | null;
46
+ refetch: () => Promise<void>;
47
+ } {
48
+ const [results, setResults] = useState<Record<Permission, boolean>>({} as Record<Permission, boolean>);
49
+ const [isLoading, setIsLoading] = useState(true);
50
+ const [error, setError] = useState<Error | null>(null);
51
+
52
+ const checkPermissions = useCallback(async () => {
53
+ if (!userId || permissions.length === 0) {
54
+ setResults({} as Record<Permission, boolean>);
55
+ setIsLoading(false);
56
+ return;
57
+ }
58
+
59
+ try {
60
+ setIsLoading(true);
61
+ setError(null);
62
+
63
+ const permissionResults: Record<Permission, boolean> = {} as Record<Permission, boolean>;
64
+
65
+ // Check each permission
66
+ for (const permission of permissions) {
67
+ const result = useCache
68
+ ? await isPermittedCached({ userId, scope, permission })
69
+ : await isPermitted({ userId, scope, permission });
70
+ permissionResults[permission] = result;
71
+ }
72
+
73
+ setResults(permissionResults);
74
+ } catch (err) {
75
+ setError(err instanceof Error ? err : new Error('Failed to check permissions'));
76
+ setResults({} as Record<Permission, boolean>);
77
+ } finally {
78
+ setIsLoading(false);
79
+ }
80
+ }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
81
+
82
+ useEffect(() => {
83
+ checkPermissions();
84
+ }, [checkPermissions]);
85
+
86
+ // Memoize the return object to prevent unnecessary re-renders
87
+ return useMemo(() => ({
88
+ results,
89
+ isLoading,
90
+ error,
91
+ refetch: checkPermissions
92
+ }), [results, isLoading, error, checkPermissions]);
93
+ }