@jmruthers/pace-core 0.5.193 → 0.6.2

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 (577) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +7 -1
  3. package/cursor-rules/00-pace-core-compliance.mdc +299 -0
  4. package/cursor-rules/01-standards-compliance.mdc +244 -0
  5. package/cursor-rules/02-project-structure.mdc +200 -0
  6. package/cursor-rules/03-solid-principles.mdc +222 -0
  7. package/cursor-rules/04-testing-standards.mdc +268 -0
  8. package/cursor-rules/05-bug-reports-and-features.mdc +246 -0
  9. package/cursor-rules/06-code-quality.mdc +309 -0
  10. package/cursor-rules/07-tech-stack-compliance.mdc +214 -0
  11. package/cursor-rules/08-markup-quality.mdc +452 -0
  12. package/cursor-rules/CHANGELOG.md +119 -0
  13. package/cursor-rules/README.md +192 -0
  14. package/dist/{AuthService-DjnJHDtC.d.ts → AuthService-BPvc3Ka0.d.ts} +54 -0
  15. package/dist/{DataTable-Be6dH_dR.d.ts → DataTable-BMRU8a1j.d.ts} +34 -2
  16. package/dist/{DataTable-5FU7IESH.js → DataTable-TPTKCX4D.js} +10 -9
  17. package/dist/{PublicPageProvider-C0Sm_e5k.d.ts → PublicPageProvider-DC6kCaqf.d.ts} +385 -261
  18. package/dist/{UnifiedAuthProvider-RGJTDE2C.js → UnifiedAuthProvider-CH6Z342H.js} +3 -3
  19. package/dist/{UnifiedAuthProvider-185Ih4dj.d.ts → UnifiedAuthProvider-CVcTjx-d.d.ts} +29 -0
  20. package/dist/{api-N774RPUA.js → api-MVVQZLJI.js} +2 -2
  21. package/dist/{chunk-KNC55RTG.js → chunk-24UVZUZG.js} +90 -54
  22. package/dist/chunk-24UVZUZG.js.map +1 -0
  23. package/dist/{chunk-HWIIPPNI.js → chunk-2UOI2FG5.js} +20 -20
  24. package/dist/chunk-2UOI2FG5.js.map +1 -0
  25. package/dist/{chunk-E3SPN4VZ 5.js → chunk-3XC4CPTD.js} +4345 -3986
  26. package/dist/chunk-3XC4CPTD.js.map +1 -0
  27. package/dist/{chunk-7EQTDTTJ.js → chunk-6J4GEEJR.js} +172 -45
  28. package/dist/chunk-6J4GEEJR.js.map +1 -0
  29. package/dist/{chunk-6C4YBBJM 5.js → chunk-6SOIHG6Z.js} +1 -1
  30. package/dist/chunk-6SOIHG6Z.js.map +1 -0
  31. package/dist/{chunk-7FLMSG37.js → chunk-EHMR7VYL.js} +25 -25
  32. package/dist/chunk-EHMR7VYL.js.map +1 -0
  33. package/dist/{chunk-I7PSE6JW.js → chunk-F2IMUDXZ.js} +2 -75
  34. package/dist/chunk-F2IMUDXZ.js.map +1 -0
  35. package/dist/{chunk-QWWZ5CAQ.js → chunk-FFQEQTNW.js} +7 -9
  36. package/dist/chunk-FFQEQTNW.js.map +1 -0
  37. package/dist/chunk-FMUCXFII.js +76 -0
  38. package/dist/chunk-FMUCXFII.js.map +1 -0
  39. package/dist/{chunk-HW3OVDUF.js → chunk-J36DSWQK.js} +1 -1
  40. package/dist/{chunk-HW3OVDUF.js.map → chunk-J36DSWQK.js.map} +1 -1
  41. package/dist/{chunk-SQGMNID3.js → chunk-L4OXEN46.js} +4 -5
  42. package/dist/chunk-L4OXEN46.js.map +1 -0
  43. package/dist/{chunk-R77UEZ4E 3.js → chunk-M43Y4SSO.js} +1 -1
  44. package/dist/chunk-M43Y4SSO.js.map +1 -0
  45. package/dist/{chunk-IIELH4DL.js → chunk-MMZ7JXPU.js} +60 -223
  46. package/dist/chunk-MMZ7JXPU.js.map +1 -0
  47. package/dist/{chunk-NOAYCWCX 5.js → chunk-NECFR5MM.js} +394 -312
  48. package/dist/chunk-NECFR5MM.js.map +1 -0
  49. package/dist/{chunk-BC4IJKSL.js → chunk-SFZUDBL5.js} +40 -4
  50. package/dist/chunk-SFZUDBL5.js.map +1 -0
  51. package/dist/{chunk-XNXXZ43G.js → chunk-XWQCNGTQ.js} +748 -364
  52. package/dist/chunk-XWQCNGTQ.js.map +1 -0
  53. package/dist/components.d.ts +6 -6
  54. package/dist/components.js +15 -12
  55. package/dist/components.js.map +1 -1
  56. package/dist/{functions-D_kgHktt.d.ts → functions-DHebl8-F.d.ts} +1 -1
  57. package/dist/hooks.d.ts +59 -126
  58. package/dist/hooks.js +19 -28
  59. package/dist/hooks.js.map +1 -1
  60. package/dist/index.d.ts +63 -16
  61. package/dist/index.js +23 -24
  62. package/dist/index.js.map +1 -1
  63. package/dist/providers.d.ts +21 -3
  64. package/dist/providers.js +2 -2
  65. package/dist/rbac/index.d.ts +146 -115
  66. package/dist/rbac/index.js +8 -11
  67. package/dist/theming/runtime.d.ts +1 -13
  68. package/dist/theming/runtime.js +1 -1
  69. package/dist/{timezone-_pgH8qrY.d.ts → timezone-CHhWg6b4.d.ts} +3 -10
  70. package/dist/{types-UU913iLA.d.ts → types-BeoeWV5I.d.ts} +8 -0
  71. package/dist/{types-CEpcvwwF.d.ts → types-CkbwOr4Y.d.ts} +6 -0
  72. package/dist/types.d.ts +2 -2
  73. package/dist/{usePublicRouteParams-TZe0gy-4.d.ts → usePublicRouteParams-1oMokgLF.d.ts} +34 -4
  74. package/dist/{useToast-C8gR5ir4.d.ts → useToast-AyaT-x7p.d.ts} +2 -2
  75. package/dist/utils.d.ts +4 -5
  76. package/dist/utils.js +15 -15
  77. package/dist/utils.js.map +1 -1
  78. package/docs/api/README.md +7 -1
  79. package/docs/api/classes/ColumnFactory.md +8 -8
  80. package/docs/api/classes/InvalidScopeError.md +4 -4
  81. package/docs/api/classes/Logger.md +1 -1
  82. package/docs/api/classes/MissingUserContextError.md +4 -4
  83. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  84. package/docs/api/classes/PermissionDeniedError.md +4 -4
  85. package/docs/api/classes/RBACAuditManager.md +1 -1
  86. package/docs/api/classes/RBACCache.md +1 -1
  87. package/docs/api/classes/RBACEngine.md +1 -1
  88. package/docs/api/classes/RBACError.md +4 -4
  89. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  90. package/docs/api/classes/SecureSupabaseClient.md +18 -15
  91. package/docs/api/classes/StorageUtils.md +1 -1
  92. package/docs/api/enums/FileCategory.md +1 -1
  93. package/docs/api/enums/LogLevel.md +1 -1
  94. package/docs/api/enums/RBACErrorCode.md +1 -1
  95. package/docs/api/enums/RPCFunction.md +1 -1
  96. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  97. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  98. package/docs/api/interfaces/AggregateConfig.md +4 -4
  99. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  100. package/docs/api/interfaces/AvatarProps.md +1 -1
  101. package/docs/api/interfaces/BadgeProps.md +9 -2
  102. package/docs/api/interfaces/ButtonProps.md +7 -4
  103. package/docs/api/interfaces/CalendarProps.md +8 -5
  104. package/docs/api/interfaces/CardProps.md +8 -5
  105. package/docs/api/interfaces/ColorPalette.md +1 -1
  106. package/docs/api/interfaces/ColorShade.md +1 -1
  107. package/docs/api/interfaces/ComplianceResult.md +1 -1
  108. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  109. package/docs/api/interfaces/DataRecord.md +1 -1
  110. package/docs/api/interfaces/DataTableAction.md +24 -21
  111. package/docs/api/interfaces/DataTableColumn.md +31 -31
  112. package/docs/api/interfaces/DataTableProps.md +1 -1
  113. package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
  114. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  115. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  116. package/docs/api/interfaces/EmptyStateConfig.md +5 -5
  117. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  118. package/docs/api/interfaces/ErrorBoundaryProps.md +147 -0
  119. package/docs/api/interfaces/ErrorBoundaryProviderProps.md +36 -0
  120. package/docs/api/interfaces/ErrorBoundaryState.md +75 -0
  121. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  122. package/docs/api/interfaces/ExportColumn.md +1 -1
  123. package/docs/api/interfaces/ExportOptions.md +8 -8
  124. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  125. package/docs/api/interfaces/FileMetadata.md +1 -1
  126. package/docs/api/interfaces/FileReference.md +1 -1
  127. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  128. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  129. package/docs/api/interfaces/FileUploadProps.md +26 -23
  130. package/docs/api/interfaces/FooterProps.md +10 -8
  131. package/docs/api/interfaces/FormFieldProps.md +10 -10
  132. package/docs/api/interfaces/FormProps.md +1 -1
  133. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  134. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  135. package/docs/api/interfaces/InputProps.md +7 -4
  136. package/docs/api/interfaces/LabelProps.md +1 -1
  137. package/docs/api/interfaces/LoggerConfig.md +1 -1
  138. package/docs/api/interfaces/LoginFormProps.md +14 -11
  139. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  140. package/docs/api/interfaces/NavigationContextType.md +1 -1
  141. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  142. package/docs/api/interfaces/NavigationItem.md +11 -11
  143. package/docs/api/interfaces/NavigationMenuProps.md +15 -15
  144. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  145. package/docs/api/interfaces/Organisation.md +1 -1
  146. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  147. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  148. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  149. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  150. package/docs/api/interfaces/PaceAppLayoutProps.md +30 -27
  151. package/docs/api/interfaces/PaceLoginPageProps.md +6 -4
  152. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  153. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  154. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  155. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  156. package/docs/api/interfaces/PaletteData.md +1 -1
  157. package/docs/api/interfaces/ParsedAddress.md +1 -1
  158. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  159. package/docs/api/interfaces/ProgressProps.md +1 -1
  160. package/docs/api/interfaces/ProtectedRouteProps.md +7 -26
  161. package/docs/api/interfaces/PublicPageFooterProps.md +9 -9
  162. package/docs/api/interfaces/PublicPageHeaderProps.md +10 -10
  163. package/docs/api/interfaces/PublicPageLayoutProps.md +7 -20
  164. package/docs/api/interfaces/QuickFix.md +1 -1
  165. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  166. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  167. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  168. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  169. package/docs/api/interfaces/RBACConfig.md +1 -1
  170. package/docs/api/interfaces/RBACContext.md +1 -1
  171. package/docs/api/interfaces/RBACLogger.md +1 -1
  172. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  173. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  174. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  175. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  176. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  177. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  178. package/docs/api/interfaces/RBACResult.md +1 -1
  179. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  180. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  181. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  182. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  183. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  184. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  185. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  186. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  187. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  188. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  189. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  190. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  191. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  192. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  193. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  194. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  195. package/docs/api/interfaces/RouteConfig.md +1 -1
  196. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  197. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  198. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  199. package/docs/api/interfaces/SessionRestorationLoaderProps.md +3 -3
  200. package/docs/api/interfaces/SetupIssue.md +1 -1
  201. package/docs/api/interfaces/StorageConfig.md +1 -1
  202. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  203. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  204. package/docs/api/interfaces/StorageListOptions.md +1 -1
  205. package/docs/api/interfaces/StorageListResult.md +1 -1
  206. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  207. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  208. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  209. package/docs/api/interfaces/StyleImport.md +1 -1
  210. package/docs/api/interfaces/SwitchProps.md +1 -1
  211. package/docs/api/interfaces/TabsContentProps.md +1 -1
  212. package/docs/api/interfaces/TabsListProps.md +1 -1
  213. package/docs/api/interfaces/TabsProps.md +1 -1
  214. package/docs/api/interfaces/TabsTriggerProps.md +3 -3
  215. package/docs/api/interfaces/TextareaProps.md +1 -1
  216. package/docs/api/interfaces/ToastActionElement.md +4 -1
  217. package/docs/api/interfaces/ToastProps.md +1 -1
  218. package/docs/api/interfaces/UnifiedAuthContextType.md +58 -55
  219. package/docs/api/interfaces/UnifiedAuthProviderProps.md +15 -13
  220. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  221. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  222. package/docs/api/interfaces/UseInactivityTrackerOptions.md +11 -9
  223. package/docs/api/interfaces/UseInactivityTrackerReturn.md +8 -8
  224. package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
  225. package/docs/api/interfaces/UsePublicEventLogoReturn.md +9 -6
  226. package/docs/api/interfaces/UsePublicEventOptions.md +3 -3
  227. package/docs/api/interfaces/UsePublicEventReturn.md +8 -5
  228. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +4 -4
  229. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +12 -9
  230. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +10 -7
  231. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  232. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  233. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  234. package/docs/api/interfaces/UserEventAccess.md +14 -11
  235. package/docs/api/interfaces/UserMenuProps.md +8 -6
  236. package/docs/api/interfaces/UserProfile.md +1 -1
  237. package/docs/api/modules.md +575 -634
  238. package/docs/architecture/database-schema-requirements.md +161 -0
  239. package/docs/core-concepts/rbac-system.md +3 -3
  240. package/docs/documentation-index.md +2 -4
  241. package/docs/getting-started/cursor-rules.md +263 -0
  242. package/docs/getting-started/installation-guide.md +6 -1
  243. package/docs/getting-started/quick-start.md +6 -1
  244. package/docs/migration/DOCUMENTATION_STRUCTURE.md +441 -0
  245. package/docs/migration/MIGRATION_GUIDE.md +6 -28
  246. package/docs/migration/README.md +52 -6
  247. package/docs/migration/V0.5.190_TO_V0.6.1_MIGRATION.md +1153 -0
  248. package/docs/migration/V0.6.0_REACT_19_MIGRATION.md +227 -0
  249. package/docs/migration/database-changes-december-2025.md +3 -3
  250. package/docs/rbac/event-based-apps.md +1 -1
  251. package/docs/rbac/getting-started.md +1 -1
  252. package/docs/rbac/quick-start.md +1 -1
  253. package/docs/standards/README.md +40 -0
  254. package/docs/troubleshooting/migration.md +4 -4
  255. package/examples/PublicPages/PublicEventPage.tsx +1 -1
  256. package/package.json +12 -6
  257. package/scripts/audit/core/checks/accessibility.cjs +197 -0
  258. package/scripts/audit/core/checks/api-usage.cjs +191 -0
  259. package/scripts/audit/core/checks/bundle.cjs +142 -0
  260. package/scripts/{check-pace-core-compliance.cjs → audit/core/checks/compliance.cjs} +737 -691
  261. package/scripts/audit/core/checks/config.cjs +54 -0
  262. package/scripts/audit/core/checks/coverage.cjs +84 -0
  263. package/scripts/audit/core/checks/dependencies.cjs +454 -0
  264. package/scripts/audit/core/checks/documentation.cjs +203 -0
  265. package/scripts/audit/core/checks/environment.cjs +128 -0
  266. package/scripts/audit/core/checks/error-handling.cjs +299 -0
  267. package/scripts/audit/core/checks/forms.cjs +172 -0
  268. package/scripts/audit/core/checks/heuristics.cjs +68 -0
  269. package/scripts/audit/core/checks/hooks.cjs +334 -0
  270. package/scripts/audit/core/checks/imports.cjs +244 -0
  271. package/scripts/audit/core/checks/performance.cjs +325 -0
  272. package/scripts/audit/core/checks/routes.cjs +117 -0
  273. package/scripts/audit/core/checks/state.cjs +130 -0
  274. package/scripts/audit/core/checks/structure.cjs +65 -0
  275. package/scripts/audit/core/checks/style.cjs +584 -0
  276. package/scripts/audit/core/checks/testing.cjs +122 -0
  277. package/scripts/audit/core/checks/typescript.cjs +61 -0
  278. package/scripts/audit/core/scanner.cjs +199 -0
  279. package/scripts/audit/core/utils.cjs +137 -0
  280. package/scripts/audit/index.cjs +223 -0
  281. package/scripts/audit/reporters/console.cjs +151 -0
  282. package/scripts/audit/reporters/json.cjs +54 -0
  283. package/scripts/audit/reporters/markdown.cjs +124 -0
  284. package/scripts/audit-consuming-app.cjs +86 -0
  285. package/scripts/build-docs/build-decision.js +240 -0
  286. package/scripts/build-docs/cache-utils.js +105 -0
  287. package/scripts/build-docs/content-normalization.js +150 -0
  288. package/scripts/build-docs/file-utils.js +105 -0
  289. package/scripts/build-docs/git-utils.js +86 -0
  290. package/scripts/build-docs/hash-utils.js +116 -0
  291. package/scripts/build-docs/typedoc-runner.js +220 -0
  292. package/scripts/build-docs-incremental.js +77 -913
  293. package/scripts/install-cursor-rules.cjs +236 -0
  294. package/scripts/utils/command-runner.js +16 -11
  295. package/scripts/validate-formats.js +61 -56
  296. package/scripts/validate-master.js +74 -69
  297. package/scripts/validate-pre-publish.js +70 -65
  298. package/src/__tests__/helpers/test-providers.tsx +1 -1
  299. package/src/__tests__/helpers/test-utils.tsx +1 -1
  300. package/src/__tests__/hooks/usePermissions.test.ts +2 -2
  301. package/src/components/Alert/Alert.test.tsx +12 -18
  302. package/src/components/Alert/Alert.tsx +5 -7
  303. package/src/components/Avatar/Avatar.test.tsx +4 -4
  304. package/src/components/Badge/Badge.tsx +16 -4
  305. package/src/components/Button/Button.tsx +27 -4
  306. package/src/components/Calendar/Calendar.tsx +9 -3
  307. package/src/components/Card/Card.tsx +4 -0
  308. package/src/components/Checkbox/Checkbox.test.tsx +12 -12
  309. package/src/components/Checkbox/Checkbox.tsx +2 -2
  310. package/src/components/DataTable/DataTable.test.tsx +57 -93
  311. package/src/components/DataTable/DataTable.tsx +40 -6
  312. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +5 -6
  313. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +29 -7
  314. package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +12 -12
  315. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +2 -3
  316. package/src/components/DataTable/components/AccessDeniedPage.tsx +17 -26
  317. package/src/components/DataTable/components/ActionButtons.tsx +10 -7
  318. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  319. package/src/components/DataTable/components/ColumnFilter.tsx +10 -0
  320. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +12 -0
  321. package/src/components/DataTable/components/DataTableBody.tsx +8 -0
  322. package/src/components/DataTable/components/DataTableCore.tsx +200 -561
  323. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +11 -0
  324. package/src/components/DataTable/components/DataTableLayout.tsx +559 -0
  325. package/src/components/DataTable/components/DataTableModals.tsx +9 -1
  326. package/src/components/DataTable/components/DataTableToolbar.tsx +8 -0
  327. package/src/components/DataTable/components/DraggableColumnHeader.tsx +12 -0
  328. package/src/components/DataTable/components/EditFields.tsx +307 -0
  329. package/src/components/DataTable/components/EditableRow.tsx +9 -1
  330. package/src/components/DataTable/components/EmptyState.tsx +10 -0
  331. package/src/components/DataTable/components/FilterRow.tsx +12 -0
  332. package/src/components/DataTable/components/GroupHeader.tsx +12 -0
  333. package/src/components/DataTable/components/GroupingDropdown.tsx +12 -0
  334. package/src/components/DataTable/components/ImportModal.tsx +7 -0
  335. package/src/components/DataTable/components/LoadingState.tsx +6 -0
  336. package/src/components/DataTable/components/PaginationControls.tsx +16 -1
  337. package/src/components/DataTable/components/RowComponent.tsx +391 -0
  338. package/src/components/DataTable/components/UnifiedTableBody.tsx +62 -852
  339. package/src/components/DataTable/components/VirtualizedDataTable.tsx +16 -4
  340. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +4 -2
  341. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +23 -23
  342. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +11 -11
  343. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +36 -36
  344. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +27 -27
  345. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +39 -39
  346. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +33 -33
  347. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +29 -29
  348. package/src/components/DataTable/components/cellValueUtils.ts +40 -0
  349. package/src/components/DataTable/components/hooks/useImportModalFocus.ts +53 -0
  350. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +126 -0
  351. package/src/components/DataTable/context/DataTableContext.tsx +50 -0
  352. package/src/components/DataTable/core/ColumnFactory.ts +31 -0
  353. package/src/components/DataTable/core/DataTableContext.tsx +32 -1
  354. package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +10 -0
  355. package/src/components/DataTable/hooks/useColumnReordering.ts +14 -2
  356. package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +10 -0
  357. package/src/components/DataTable/hooks/useDataTableDataPipeline.ts +16 -0
  358. package/src/components/DataTable/hooks/useDataTablePermissions.ts +124 -32
  359. package/src/components/DataTable/hooks/useDataTableState.ts +35 -1
  360. package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +12 -0
  361. package/src/components/DataTable/hooks/useKeyboardNavigation.ts +2 -2
  362. package/src/components/DataTable/hooks/useServerSideDataEffect.ts +11 -0
  363. package/src/components/DataTable/hooks/useTableColumns.ts +8 -0
  364. package/src/components/DataTable/hooks/useTableHandlers.ts +14 -0
  365. package/src/components/DataTable/styles.ts +6 -6
  366. package/src/components/DataTable/types.ts +6 -10
  367. package/src/components/DataTable/utils/a11yUtils.ts +7 -0
  368. package/src/components/DataTable/utils/debugTools.ts +18 -113
  369. package/src/components/DataTable/utils/errorHandling.ts +12 -0
  370. package/src/components/DataTable/utils/exportUtils.ts +9 -0
  371. package/src/components/DataTable/utils/flexibleImport.ts +12 -48
  372. package/src/components/DataTable/utils/paginationUtils.ts +8 -0
  373. package/src/components/DataTable/utils/performanceUtils.ts +5 -1
  374. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -14
  375. package/src/components/Dialog/Dialog.tsx +8 -7
  376. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +180 -1
  377. package/src/components/ErrorBoundary/ErrorBoundary.tsx +46 -6
  378. package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +129 -0
  379. package/src/components/ErrorBoundary/index.ts +27 -2
  380. package/src/components/EventSelector/EventSelector.tsx +4 -1
  381. package/src/components/FileDisplay/FileDisplay.test.tsx +2 -2
  382. package/src/components/FileDisplay/FileDisplay.tsx +32 -18
  383. package/src/components/FileUpload/FileUpload.tsx +22 -2
  384. package/src/components/Footer/Footer.test.tsx +16 -16
  385. package/src/components/Footer/Footer.tsx +15 -12
  386. package/src/components/Form/Form.test.tsx +36 -15
  387. package/src/components/Form/Form.tsx +31 -26
  388. package/src/components/Header/Header.tsx +22 -11
  389. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +40 -40
  390. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +1 -1
  391. package/src/components/Input/Input.test.tsx +2 -2
  392. package/src/components/Input/Input.tsx +36 -34
  393. package/src/components/Label/Label.tsx +1 -1
  394. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +4 -4
  395. package/src/components/LoadingSpinner/LoadingSpinner.tsx +1 -1
  396. package/src/components/LoginForm/LoginForm.test.tsx +42 -42
  397. package/src/components/LoginForm/LoginForm.tsx +12 -8
  398. package/src/components/NavigationMenu/NavigationMenu.tsx +15 -514
  399. package/src/components/NavigationMenu/types.ts +56 -0
  400. package/src/components/NavigationMenu/useNavigationFiltering.ts +390 -0
  401. package/src/components/OrganisationSelector/OrganisationSelector.tsx +3 -0
  402. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +1 -1
  403. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +54 -52
  404. package/src/components/PaceAppLayout/PaceAppLayout.tsx +33 -12
  405. package/src/components/PaceAppLayout/README.md +1 -1
  406. package/src/components/PaceAppLayout/test-setup.tsx +1 -2
  407. package/src/components/PaceLoginPage/PaceLoginPage.tsx +4 -1
  408. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +33 -33
  409. package/src/components/PasswordChange/PasswordChangeForm.tsx +10 -1
  410. package/src/components/Progress/Progress.tsx +1 -1
  411. package/src/components/ProtectedRoute/ProtectedRoute.tsx +3 -9
  412. package/src/components/PublicLayout/PublicPageLayout.tsx +3 -6
  413. package/src/components/PublicLayout/PublicPageProvider.tsx +4 -0
  414. package/src/components/Select/Select.tsx +95 -438
  415. package/src/components/Select/context.ts +23 -0
  416. package/src/components/Select/hooks/useSelectEvents.ts +87 -0
  417. package/src/components/Select/hooks/useSelectSearch.ts +91 -0
  418. package/src/components/Select/hooks/useSelectState.ts +104 -0
  419. package/src/components/Select/index.ts +9 -1
  420. package/src/components/Select/types.ts +123 -0
  421. package/src/components/Select/utils/text.ts +26 -0
  422. package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +5 -6
  423. package/src/components/Switch/Switch.tsx +4 -4
  424. package/src/components/Table/Table.tsx +1 -1
  425. package/src/components/Tabs/Tabs.tsx +1 -1
  426. package/src/components/Textarea/Textarea.tsx +27 -29
  427. package/src/components/Toast/Toast.tsx +5 -1
  428. package/src/components/Tooltip/Tooltip.tsx +3 -3
  429. package/src/components/UserMenu/UserMenu.test.tsx +24 -11
  430. package/src/components/UserMenu/UserMenu.tsx +22 -19
  431. package/src/components/index.ts +2 -2
  432. package/src/hooks/__tests__/hooks.integration.test.tsx +80 -55
  433. package/src/hooks/__tests__/index.unit.test.ts +2 -5
  434. package/src/hooks/__tests__/useStorage.unit.test.ts +36 -36
  435. package/src/hooks/index.ts +1 -2
  436. package/src/hooks/public/usePublicEvent.ts +5 -1
  437. package/src/hooks/public/usePublicEventLogo.ts +5 -1
  438. package/src/hooks/public/usePublicFileDisplay.ts +4 -0
  439. package/src/hooks/public/usePublicRouteParams.ts +5 -1
  440. package/src/hooks/services/useAuth.ts +32 -0
  441. package/src/hooks/services/useCurrentEvent.ts +6 -0
  442. package/src/hooks/services/useCurrentOrganisation.ts +6 -0
  443. package/src/hooks/useDataTableState.ts +8 -18
  444. package/src/hooks/useDebounce.ts +9 -0
  445. package/src/hooks/useEventTheme.ts +6 -0
  446. package/src/hooks/useFileDisplay.ts +4 -0
  447. package/src/hooks/useFileReference.ts +25 -7
  448. package/src/hooks/useFileUrl.ts +11 -1
  449. package/src/hooks/useFocusManagement.ts +16 -2
  450. package/src/hooks/useFocusTrap.ts +7 -4
  451. package/src/hooks/useFormDialog.ts +8 -7
  452. package/src/hooks/useInactivityTracker.ts +4 -1
  453. package/src/hooks/useKeyboardShortcuts.ts +4 -0
  454. package/src/hooks/useOrganisationPermissions.ts +4 -0
  455. package/src/hooks/useOrganisationSecurity.ts +4 -0
  456. package/src/hooks/usePerformanceMonitor.ts +4 -0
  457. package/src/hooks/usePermissionCache.ts +8 -1
  458. package/src/hooks/useQueryCache.ts +12 -1
  459. package/src/hooks/useSessionRestoration.ts +4 -0
  460. package/src/hooks/useStorage.ts +4 -0
  461. package/src/hooks/useToast.ts +3 -3
  462. package/src/index.ts +2 -1
  463. package/src/providers/__tests__/OrganisationProvider.test.tsx +115 -49
  464. package/src/providers/__tests__/ProviderLifecycle.test.tsx +21 -6
  465. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +10 -10
  466. package/src/providers/services/AuthServiceProvider.tsx +18 -0
  467. package/src/providers/services/EventServiceProvider.tsx +18 -0
  468. package/src/providers/services/InactivityServiceProvider.tsx +18 -0
  469. package/src/providers/services/OrganisationServiceProvider.tsx +18 -0
  470. package/src/providers/services/UnifiedAuthProvider.tsx +58 -22
  471. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +33 -7
  472. package/src/rbac/README.md +1 -1
  473. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +26 -26
  474. package/src/rbac/__tests__/scenarios.user-role.test.tsx +4 -5
  475. package/src/rbac/adapters.tsx +14 -5
  476. package/src/rbac/api.ts +100 -67
  477. package/src/rbac/components/EnhancedNavigationMenu.tsx +1 -1
  478. package/src/rbac/components/NavigationGuard.tsx +1 -1
  479. package/src/rbac/components/NavigationProvider.tsx +5 -2
  480. package/src/rbac/components/PagePermissionGuard.tsx +158 -18
  481. package/src/rbac/components/PagePermissionProvider.tsx +1 -1
  482. package/src/rbac/components/PermissionEnforcer.tsx +1 -1
  483. package/src/rbac/components/RoleBasedRouter.tsx +6 -2
  484. package/src/rbac/components/SecureDataProvider.test.tsx +84 -49
  485. package/src/rbac/components/SecureDataProvider.tsx +21 -6
  486. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +24 -14
  487. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +7 -0
  488. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +14 -6
  489. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +15 -4
  490. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +148 -24
  491. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +81 -15
  492. package/src/rbac/engine.ts +38 -14
  493. package/src/rbac/hooks/permissions/index.ts +7 -0
  494. package/src/rbac/hooks/permissions/useAccessLevel.ts +105 -0
  495. package/src/rbac/hooks/permissions/useCachedPermissions.ts +79 -0
  496. package/src/rbac/hooks/permissions/useCan.ts +347 -0
  497. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +90 -0
  498. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +90 -0
  499. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +93 -0
  500. package/src/rbac/hooks/permissions/usePermissions.ts +253 -0
  501. package/src/rbac/hooks/useCan.test.ts +71 -64
  502. package/src/rbac/hooks/usePermissions.ts +14 -995
  503. package/src/rbac/hooks/useResourcePermissions.test.ts +54 -18
  504. package/src/rbac/hooks/useResourcePermissions.ts +14 -4
  505. package/src/rbac/hooks/useSecureSupabase.ts +33 -13
  506. package/src/rbac/permissions.ts +0 -30
  507. package/src/rbac/secureClient.ts +212 -61
  508. package/src/rbac/types.ts +8 -0
  509. package/src/theming/__tests__/parseEventColours.test.ts +6 -9
  510. package/src/theming/parseEventColours.ts +5 -19
  511. package/src/types/vitest-globals.d.ts +51 -26
  512. package/src/utils/__mocks__/supabaseMock.ts +1 -3
  513. package/src/utils/__tests__/formatting.unit.test.ts +4 -4
  514. package/src/utils/__tests__/index.unit.test.ts +2 -2
  515. package/src/utils/audit/audit.ts +0 -3
  516. package/src/utils/core/cn.ts +1 -1
  517. package/src/utils/file-reference/index.ts +53 -1
  518. package/src/utils/formatting/formatting.ts +8 -18
  519. package/src/utils/index.ts +0 -1
  520. package/src/utils/security/secureDataAccess.test.ts +31 -20
  521. package/src/utils/security/secureDataAccess.ts +4 -3
  522. package/dist/chunk-6C4YBBJM.js +0 -628
  523. package/dist/chunk-6C4YBBJM.js.map +0 -1
  524. package/dist/chunk-7D4SUZUM.js 2.map +0 -1
  525. package/dist/chunk-7EQTDTTJ.js 2.map +0 -1
  526. package/dist/chunk-7EQTDTTJ.js.map +0 -1
  527. package/dist/chunk-7FLMSG37.js 2.map +0 -1
  528. package/dist/chunk-7FLMSG37.js.map +0 -1
  529. package/dist/chunk-BC4IJKSL.js.map +0 -1
  530. package/dist/chunk-E3SPN4VZ.js +0 -12917
  531. package/dist/chunk-E3SPN4VZ.js.map +0 -1
  532. package/dist/chunk-E66EQZE6 5.js +0 -37
  533. package/dist/chunk-E66EQZE6.js 2.map +0 -1
  534. package/dist/chunk-HWIIPPNI.js.map +0 -1
  535. package/dist/chunk-I7PSE6JW 5.js +0 -191
  536. package/dist/chunk-I7PSE6JW.js 2.map +0 -1
  537. package/dist/chunk-I7PSE6JW.js.map +0 -1
  538. package/dist/chunk-IIELH4DL.js.map +0 -1
  539. package/dist/chunk-KNC55RTG.js 5.map +0 -1
  540. package/dist/chunk-KNC55RTG.js.map +0 -1
  541. package/dist/chunk-KQCRWDSA.js 5.map +0 -1
  542. package/dist/chunk-LFNCN2SP.js +0 -412
  543. package/dist/chunk-LFNCN2SP.js 2.map +0 -1
  544. package/dist/chunk-LFNCN2SP.js.map +0 -1
  545. package/dist/chunk-LMC26NLJ 2.js +0 -84
  546. package/dist/chunk-NOAYCWCX.js +0 -4993
  547. package/dist/chunk-NOAYCWCX.js.map +0 -1
  548. package/dist/chunk-QWWZ5CAQ.js 3.map +0 -1
  549. package/dist/chunk-QWWZ5CAQ.js.map +0 -1
  550. package/dist/chunk-QXHPKYJV 3.js +0 -113
  551. package/dist/chunk-R77UEZ4E.js +0 -68
  552. package/dist/chunk-R77UEZ4E.js.map +0 -1
  553. package/dist/chunk-SQGMNID3.js.map +0 -1
  554. package/dist/chunk-VBXEHIUJ.js 6.map +0 -1
  555. package/dist/chunk-XNXXZ43G.js.map +0 -1
  556. package/dist/chunk-ZSAAAMVR 6.js +0 -25
  557. package/dist/components.js 5.map +0 -1
  558. package/dist/styles/index 2.js +0 -12
  559. package/dist/styles/index.js 5.map +0 -1
  560. package/dist/theming/runtime 5.js +0 -19
  561. package/dist/theming/runtime.js 5.map +0 -1
  562. package/docs/api/classes/ErrorBoundary.md +0 -144
  563. package/docs/migration/quick-migration-guide.md +0 -356
  564. package/docs/migration/service-architecture.md +0 -281
  565. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +0 -680
  566. package/src/hooks/useSecureDataAccess.test.ts +0 -559
  567. package/src/hooks/useSecureDataAccess.ts +0 -666
  568. /package/dist/{DataTable-5FU7IESH.js.map → DataTable-TPTKCX4D.js.map} +0 -0
  569. /package/dist/{UnifiedAuthProvider-RGJTDE2C.js.map → UnifiedAuthProvider-CH6Z342H.js.map} +0 -0
  570. /package/dist/{api-N774RPUA.js.map → api-MVVQZLJI.js.map} +0 -0
  571. /package/docs/migration/{organisation-context-timing-fix.md → V0.3.44_organisation-context-timing-fix.md} +0 -0
  572. /package/docs/migration/{rbac-migration.md → V0.4.0_rbac-migration.md} +0 -0
  573. /package/docs/migration/{person-scoped-profiles-migration-guide.md → V0.5.190_person-scoped-profiles-migration-guide.md} +0 -0
  574. /package/examples/{rbac → RBAC}/CompleteRBACExample.tsx +0 -0
  575. /package/examples/{rbac → RBAC}/EventBasedApp.tsx +0 -0
  576. /package/examples/{rbac → RBAC}/PermissionExample.tsx +0 -0
  577. /package/examples/{rbac → RBAC}/index.ts +0 -0
@@ -1,996 +1,15 @@
1
1
  /**
2
- * @file RBAC Permission Hooks
3
- * @package @jmruthers/pace-core
4
- * @module RBAC/Hooks
5
- * @since 1.0.0
6
- *
7
- * This module provides React hooks for RBAC functionality.
8
- */
9
-
10
- import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
11
- import {
12
- UUID,
13
- Scope,
14
- Permission,
15
- PermissionMap,
16
- OrganisationContextRequiredError
17
- } from '../types';
18
- import { AccessLevel as AccessLevelType } from '../types';
19
- import {
20
- getAccessLevel,
21
- getPermissionMap,
22
- isPermitted,
23
- isPermittedCached
24
- } from '../api';
25
- import { getRBACLogger } from '../config';
26
- import { scopeEqual } from '../utils/deep-equal';
27
- import { useAppConfig } from '../../hooks/useAppConfig';
28
-
29
- /**
30
- * Hook to get user's permissions in a scope
31
- *
32
- * @param userId - User ID
33
- * @param organisationId - Organisation ID
34
- * @param eventId - Event ID (optional)
35
- * @param appId - Application ID (optional)
36
- * @returns Permission state and methods
37
- *
38
- * @example
39
- * ```tsx
40
- * function MyComponent() {
41
- * const { permissions, isLoading, error } = usePermissions(
42
- * userId,
43
- * organisationId,
44
- * eventId,
45
- * appId
46
- * );
47
- *
48
- * if (isLoading) return <div>Loading...</div>;
49
- * if (error) return <div>Error: {error.message}</div>;
50
- *
51
- * return (
52
- * <div>
53
- * {permissions['read:users'] && <UserList />}
54
- * {permissions['create:users'] && <CreateUserButton />}
55
- * </div>
56
- * );
57
- * }
58
- * ```
59
- */
60
- export function usePermissions(
61
- userId: UUID,
62
- organisationId: string | undefined,
63
- eventId: string | undefined,
64
- appId: string | undefined
65
- ) {
66
- const [permissions, setPermissions] = useState<PermissionMap>({} as PermissionMap);
67
- const [isLoading, setIsLoading] = useState(true);
68
- const [error, setError] = useState<Error | null>(null);
69
- const [fetchTrigger, setFetchTrigger] = useState(0);
70
- const isFetchingRef = useRef(false);
71
- const logger = getRBACLogger();
72
-
73
- // Track previous values to detect changes imperatively
74
- const prevValuesRef = useRef({ userId, organisationId, eventId, appId });
75
-
76
- // Normalize organisationId to empty string if undefined
77
- const orgId = organisationId || '';
78
-
79
- // Removed excessive logging - only log when scope actually changes (not on every render)
80
-
81
- // Add timeout for missing organisation context (3 seconds)
82
- // OPTIMIZATION: Skip timeout if userId is null/undefined (indicates pre-filtered mode)
83
- useEffect(() => {
84
- // If userId is null/undefined, skip the timeout - this indicates items are pre-filtered
85
- // and we don't need to wait for organisation context
86
- if (!userId) {
87
- return; // Skip timeout when userId is null (pre-filtered mode)
88
- }
89
-
90
- if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
91
- const timeoutId = setTimeout(() => {
92
- setError(new Error('Organisation context is required for permission checks'));
93
- setIsLoading(false);
94
- }, 3000); // 3 seconds - typical permission check is < 1 second
95
-
96
- return () => clearTimeout(timeoutId);
97
- }
98
- // Clear error if organisation context becomes available
99
- if (error?.message === 'Organisation context is required for permission checks') {
100
- setError(null);
101
- }
102
- }, [userId, organisationId, error, orgId]);
103
-
104
- // CRITICAL: Detect parameter changes and trigger fetch
105
- // Moved to useEffect to prevent render-time state updates that could cause render loops
106
- useEffect(() => {
107
- const paramsChanged =
108
- prevValuesRef.current.userId !== userId ||
109
- prevValuesRef.current.organisationId !== organisationId ||
110
- prevValuesRef.current.eventId !== eventId ||
111
- prevValuesRef.current.appId !== appId;
112
-
113
- if (paramsChanged) {
114
- // Only log significant changes (appId changes are most important)
115
- if (prevValuesRef.current.appId !== appId) {
116
- // AppId changed - triggering fetch
117
- }
118
- prevValuesRef.current = { userId, organisationId, eventId, appId };
119
- // Increment counter to force fetch useEffect to run
120
- setFetchTrigger(prev => prev + 1);
121
- }
122
- }, [userId, organisationId, eventId, appId, logger]);
123
-
124
- useEffect(() => {
125
- const fetchPermissions = async () => {
126
- // Prevent multiple simultaneous fetches
127
- if (isFetchingRef.current) {
128
- return;
129
- }
130
-
131
- if (!userId) {
132
- setPermissions({} as PermissionMap);
133
- setIsLoading(false);
134
- return;
135
- }
136
-
137
- // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
138
- // Wait for organisation context to resolve
139
- // IMPORTANT: Don't clear existing permissions here - keep them until we have new ones
140
- // OPTIMIZATION: If userId is null/undefined, immediately set loading to false
141
- // This indicates pre-filtered mode where we don't need to wait for organisation context
142
- if (!userId) {
143
- setPermissions({} as PermissionMap);
144
- setIsLoading(false);
145
- return;
146
- }
147
-
148
- if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
149
- // Keep existing permissions, just mark as loading
150
- setIsLoading(true);
151
- setError(null);
152
- return;
153
- }
154
-
155
- try {
156
- isFetchingRef.current = true;
157
- setIsLoading(true);
158
- setError(null);
159
-
160
- // Build scope object for API call
161
- const scope: Scope = {
162
- organisationId: orgId,
163
- eventId: eventId,
164
- appId: appId
165
- };
166
-
167
- // Fetch new permissions - don't clear old ones until we have new ones
168
- const permissionMap = await getPermissionMap({ userId, scope });
169
-
170
- // Only log if there's a significant change or error
171
- const permissionCount = Object.keys(permissionMap).length;
172
- if (permissionCount === 0 && Object.keys(permissions).length > 0) {
173
- logger.warn('[usePermissions] Permissions fetched but returned empty map', {
174
- scope: { organisationId: orgId, eventId, appId }
175
- });
176
- }
177
-
178
- // Only update permissions if fetch was successful
179
- setPermissions(permissionMap);
180
- } catch (err) {
181
- // On error, keep existing permissions but set error state
182
- // This prevents the UI from losing all items when there's a transient error
183
- logger.error('[usePermissions] Failed to fetch permissions:', err);
184
- setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
185
- // Don't clear permissions on error - keep what we had
186
- } finally {
187
- setIsLoading(false);
188
- isFetchingRef.current = false;
189
- }
190
- };
191
-
192
- fetchPermissions();
193
- }, [fetchTrigger, userId, organisationId, eventId, appId]);
194
-
195
- const hasPermission = useCallback((permission: Permission): boolean => {
196
- if (permissions['*']) {
197
- return true;
198
- }
199
- return permissions[permission] === true;
200
- }, [permissions]);
201
-
202
- const hasAnyPermission = useCallback((permissionList: Permission[]): boolean => {
203
- if (permissions['*']) {
204
- return true;
205
- }
206
- return permissionList.some(p => permissions[p] === true);
207
- }, [permissions]);
208
-
209
- const hasAllPermissions = useCallback((permissionList: Permission[]): boolean => {
210
- if (permissions['*']) {
211
- return true;
212
- }
213
- return permissionList.every(p => permissions[p] === true);
214
- }, [permissions]);
215
-
216
- const refetch = useCallback(async () => {
217
- // Prevent multiple simultaneous fetches
218
- if (isFetchingRef.current) {
219
- return;
220
- }
221
-
222
- if (!userId) {
223
- setPermissions({} as PermissionMap);
224
- setIsLoading(false);
225
- return;
226
- }
227
-
228
- // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
229
- // IMPORTANT: Don't clear existing permissions - keep them until we have new ones
230
- if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
231
- // Keep existing permissions, just mark as loading
232
- setIsLoading(true);
233
- setError(null);
234
- return;
235
- }
236
-
237
- try {
238
- isFetchingRef.current = true;
239
- setIsLoading(true);
240
- setError(null);
241
-
242
- // Build scope object for API call
243
- const scope: Scope = {
244
- organisationId: orgId,
245
- eventId: eventId,
246
- appId: appId
247
- };
248
-
249
- // Fetch new permissions - don't clear old ones until we have new ones
250
- const permissionMap = await getPermissionMap({ userId, scope });
251
-
252
- // Only update permissions if fetch was successful
253
- setPermissions(permissionMap);
254
- } catch (err) {
255
- // On error, keep existing permissions but set error state
256
- // This prevents the UI from losing all items when there's a transient error
257
- const logger = getRBACLogger();
258
- logger.error('Failed to refetch permissions:', err);
259
- setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
260
- // Don't clear permissions on error - keep what we had
261
- } finally {
262
- setIsLoading(false);
263
- isFetchingRef.current = false;
264
- }
265
- }, [userId, organisationId, eventId, appId]);
266
-
267
- // Memoize the return object to prevent unnecessary re-renders
268
- return useMemo(() => ({
269
- permissions,
270
- isLoading,
271
- error,
272
- hasPermission,
273
- hasAnyPermission,
274
- hasAllPermissions,
275
- refetch
276
- }), [permissions, isLoading, error, hasPermission, hasAnyPermission, hasAllPermissions, refetch]);
277
- }
278
-
279
- /**
280
- * Hook to check if user can perform an action
281
- *
282
- * @param userId - User ID
283
- * @param scope - Scope for permission checking
284
- * @param permission - Permission to check
285
- * @param pageId - Optional page ID
286
- * @param useCache - Whether to use cached results
287
- * @param appName - Optional app name (for PORTAL/ADMIN special case)
288
- * @returns Permission check state and methods
289
- *
290
- * @example
291
- * ```tsx
292
- * function MyComponent() {
293
- * const { can, isLoading, error } = useCan(userId, scope, 'read:users');
294
- *
295
- * if (isLoading) return <div>Checking permission...</div>;
296
- * if (error) return <div>Error: {error.message}</div>;
297
- *
298
- * return can ? <UserList /> : <div>Access denied</div>;
299
- * }
300
- * ```
301
- */
302
- export function useCan(
303
- userId: UUID,
304
- scope: Scope,
305
- permission: Permission,
306
- pageId?: UUID,
307
- useCache: boolean = true,
308
- appName?: string
309
- ) {
310
- const [can, setCan] = useState<boolean>(false);
311
- const [isLoading, setIsLoading] = useState(true);
312
- const [error, setError] = useState<Error | null>(null);
313
- const [isSuperAdmin, setIsSuperAdmin] = useState<boolean | null>(null);
314
-
315
- // Validate scope parameter - handle undefined/null scope gracefully
316
- const isValidScope = scope && typeof scope === 'object';
317
- const organisationId = isValidScope ? scope.organisationId : undefined;
318
- const eventId = isValidScope ? scope.eventId : undefined;
319
- const appId = isValidScope ? scope.appId : undefined;
320
-
321
- // Check super-admin status - super admins bypass organisation context requirements
322
- useEffect(() => {
323
- if (!userId) {
324
- setIsSuperAdmin(false);
325
- return;
326
- }
327
-
328
- let cancelled = false;
329
- const checkSuperAdmin = async () => {
330
- try {
331
- const { isSuperAdmin: checkSuperAdmin } = await import('../api');
332
- const isSuper = await checkSuperAdmin(userId);
333
- if (!cancelled) {
334
- setIsSuperAdmin(isSuper);
335
- }
336
- } catch (err) {
337
- if (!cancelled) {
338
- setIsSuperAdmin(false);
339
- }
340
- }
341
- };
342
-
343
- checkSuperAdmin();
344
- return () => {
345
- cancelled = true;
346
- };
347
- }, [userId]);
348
-
349
- // Add timeout for missing organisation context (3 seconds)
350
- // Only apply timeout for resource-level permissions, not page-level (which can handle null orgs)
351
- // Super admins bypass this check
352
- useEffect(() => {
353
- const isPagePermission = permission.includes(':page.') || !!pageId;
354
- const requiresOrgId = !isPagePermission;
355
-
356
- // Don't block if user is super-admin (they bypass context requirements)
357
- if (isSuperAdmin === true) {
358
- return;
359
- }
360
-
361
- if (requiresOrgId && (!isValidScope || !organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
362
- const timeoutId = setTimeout(() => {
363
- setError(new Error('Organisation context is required for permission checks'));
364
- setIsLoading(false);
365
- setCan(false);
366
- }, 3000); // 3 seconds - typical permission check is < 1 second
367
-
368
- return () => clearTimeout(timeoutId);
369
- }
370
- // Clear error if organisation context becomes available
371
- if (error?.message === 'Organisation context is required for permission checks') {
372
- setError(null);
373
- }
374
- }, [isValidScope, organisationId, error, permission, pageId, isSuperAdmin]);
375
-
376
- // Use refs to track the last values to prevent unnecessary re-runs
377
- const lastUserIdRef = useRef<UUID | null>(null);
378
- const lastScopeRef = useRef<string | null>(null);
379
- const lastPermissionRef = useRef<Permission | null>(null);
380
- const lastPageIdRef = useRef<UUID | undefined | null>(null);
381
- const lastUseCacheRef = useRef<boolean | null>(null);
382
-
383
- // Create a stable scope object for comparison
384
- const stableScope = useMemo(() => {
385
- if (!isValidScope) {
386
- return null;
387
- }
388
- return {
389
- organisationId,
390
- eventId,
391
- appId,
392
- };
393
- }, [isValidScope, organisationId, eventId, appId]);
394
-
395
- // Track previous scope for deep equality comparison
396
- const prevScopeRef = useRef<Scope | null>(null);
397
-
398
- useEffect(() => {
399
- // Use deep equality check for scope to prevent unnecessary re-runs
400
- const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
401
-
402
- // Only run if something has actually changed
403
- if (
404
- lastUserIdRef.current !== userId ||
405
- scopeChanged ||
406
- lastPermissionRef.current !== permission ||
407
- lastPageIdRef.current !== pageId ||
408
- lastUseCacheRef.current !== useCache
409
- ) {
410
- lastUserIdRef.current = userId;
411
- prevScopeRef.current = stableScope;
412
- lastPermissionRef.current = permission;
413
- lastPageIdRef.current = pageId;
414
- lastUseCacheRef.current = useCache;
415
-
416
- // Inline the permission check logic to avoid useCallback dependency issues
417
- const checkPermission = async () => {
418
- if (!userId) {
419
- setCan(false);
420
- setIsLoading(false);
421
- return;
422
- }
423
-
424
- // Validate scope before accessing properties
425
- if (!isValidScope) {
426
- setIsLoading(true);
427
- setCan(false);
428
- setError(null);
429
- // Timeout is handled in separate useEffect
430
- return;
431
- }
432
-
433
- // For page-level permissions, allow undefined/null organisationId (database function handles it)
434
- // For resource-level permissions, organisationId is required
435
- const isPagePermission = permission.includes(':page.') || !!pageId;
436
- const requiresOrgId = !isPagePermission;
437
-
438
- // Check if pageId is a pageName (not a UUID) - if so, we need appId to resolve it
439
- 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);
440
- const needsAppIdForPageName = isPagePermission && isPageName;
441
-
442
- // Don't check permissions if scope is invalid and orgId is required
443
- // Wait for organisation context to resolve (unless it's a page permission that can handle null orgs)
444
- // Super admins bypass this check - only proceed if super-admin status is confirmed to be true
445
- // If super-admin status is still being checked (null), wait for it to complete
446
- if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
447
- // Only proceed if user is confirmed to be super-admin
448
- if (isSuperAdmin === true) {
449
- // Super-admin bypass - allow check to proceed (isPermitted will handle super-admin bypass)
450
- } else {
451
- // Not super-admin or still checking - wait for org context or super-admin check to complete
452
- setIsLoading(true);
453
- setCan(false);
454
- setError(null);
455
- // Timeout is handled in separate useEffect (Phase 1.4)
456
- return;
457
- }
458
- }
459
-
460
- // For page-level permissions with pageName (not UUID), we need appId to resolve the pageName to pageId
461
- // Wait for appId to be available before checking permissions
462
- if (needsAppIdForPageName && (!appId || appId === null || (typeof appId === 'string' && appId.trim() === ''))) {
463
- setIsLoading(true);
464
- setCan(false);
465
- setError(null);
466
- // Will re-run when appId becomes available (via scope change detection)
467
- return;
468
- }
469
-
470
- try {
471
- setIsLoading(true);
472
- setError(null);
473
-
474
- // Create a valid scope object for the API call
475
- // For page-level permissions, organisationId can be undefined (database handles it)
476
- const validScope: Scope = {
477
- ...(organisationId ? { organisationId } : {}),
478
- ...(eventId ? { eventId } : {}),
479
- ...(appId ? { appId } : {})
480
- };
481
-
482
- const result = useCache
483
- ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
484
- : await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName);
485
-
486
- setCan(result);
487
- } catch (err) {
488
- const logger = getRBACLogger();
489
- logger.error('Permission check error:', { permission, error: err });
490
- setError(err instanceof Error ? err : new Error('Failed to check permission'));
491
- setCan(false);
492
- } finally {
493
- setIsLoading(false);
494
- }
495
- };
496
-
497
- checkPermission();
498
- }
499
- }, [userId, stableScope, permission, pageId, useCache, appName, isSuperAdmin]);
500
-
501
- const refetch = useCallback(async () => {
502
- if (!userId) {
503
- setCan(false);
504
- setIsLoading(false);
505
- return;
506
- }
507
-
508
- // Validate scope before accessing properties
509
- if (!isValidScope) {
510
- setCan(false);
511
- setIsLoading(true);
512
- setError(null);
513
- return;
514
- }
515
-
516
- // For page-level permissions, allow undefined/null organisationId (database function handles it)
517
- // For resource-level permissions, organisationId is required
518
- const isPagePermission = permission.includes(':page.') || !!pageId;
519
- const requiresOrgId = !isPagePermission;
520
-
521
- // Don't check permissions if scope is invalid and orgId is required
522
- if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
523
- setCan(false);
524
- setIsLoading(true);
525
- setError(null);
526
- return;
527
- }
528
-
529
- try {
530
- setIsLoading(true);
531
- setError(null);
532
-
533
- // Create a valid scope object for the API call
534
- // For page-level permissions, organisationId can be undefined (database handles it)
535
- const validScope: Scope = {
536
- ...(organisationId ? { organisationId } : {}),
537
- ...(eventId ? { eventId } : {}),
538
- ...(appId ? { appId } : {})
539
- };
540
-
541
- const result = useCache
542
- ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
543
- : await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName);
544
-
545
- setCan(result);
546
- } catch (err) {
547
- setError(err instanceof Error ? err : new Error('Failed to check permission'));
548
- setCan(false);
549
- } finally {
550
- setIsLoading(false);
551
- }
552
- }, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache, appName]);
553
-
554
- // Memoize the return object to prevent unnecessary re-renders
555
- return useMemo(() => ({
556
- can,
557
- isLoading,
558
- error,
559
- refetch
560
- }), [can, isLoading, error, refetch]);
561
- }
562
-
563
- /**
564
- * Hook to get user's access level in a scope
565
- *
566
- * @param userId - User ID
567
- * @param scope - Scope for access level checking
568
- * @returns Access level state and methods
569
- *
570
- * @example
571
- * ```tsx
572
- * function MyComponent() {
573
- * const { accessLevel, isLoading, error } = useAccessLevel(userId, scope);
574
- *
575
- * if (isLoading) return <div>Loading access level...</div>;
576
- * if (error) return <div>Error: {error.message}</div>;
577
- *
578
- * return (
579
- * <div>
580
- * Access Level: {accessLevel}
581
- * {accessLevel >= AccessLevel.ADMIN && <AdminPanel />}
582
- * </div>
583
- * );
584
- * }
585
- * ```
586
- */
587
- export function useAccessLevel(userId: UUID, scope: Scope): {
588
- accessLevel: AccessLevelType;
589
- isLoading: boolean;
590
- error: Error | null;
591
- refetch: () => Promise<void>;
592
- } {
593
- const [accessLevel, setAccessLevel] = useState<AccessLevelType>('viewer');
594
- const [isLoading, setIsLoading] = useState(true);
595
- const [error, setError] = useState<Error | null>(null);
596
-
597
- // Get appName from context if available (safely handles missing context)
598
- let appName: string | undefined;
599
- try {
600
- const { appName: contextAppName } = useAppConfig();
601
- appName = contextAppName;
602
- } catch {
603
- // Not available, will use undefined
604
- }
605
-
606
- const fetchAccessLevel = useCallback(async () => {
607
- if (!userId) {
608
- setAccessLevel('viewer');
609
- setIsLoading(false);
610
- return;
611
- }
612
-
613
- try {
614
- setIsLoading(true);
615
- setError(null);
616
-
617
- // Check super admin status first - super admins bypass context requirements
618
- // This allows super admins to check their access level without organisation context
619
- const { isSuperAdmin: checkSuperAdmin } = await import('../api');
620
- const isSuperAdminUser = await checkSuperAdmin(userId);
621
-
622
- if (isSuperAdminUser) {
623
- setAccessLevel('super');
624
- setIsLoading(false);
625
- return;
626
- }
627
-
628
- // Early validation: check if scope has required context
629
- // PORTAL/ADMIN apps allow both contexts to be optional
630
- if (appName !== 'PORTAL' && appName !== 'ADMIN' && !scope.organisationId && !scope.eventId) {
631
- const orgError = new OrganisationContextRequiredError();
632
- setError(orgError);
633
- setAccessLevel('viewer');
634
- setIsLoading(false);
635
- return;
636
- }
637
-
638
- const level = await getAccessLevel({ userId, scope }, null, appName);
639
- setAccessLevel(level);
640
- } catch (err) {
641
- const error = err instanceof Error ? err : new Error('Failed to fetch access level');
642
- setError(error);
643
- setAccessLevel('viewer');
644
- } finally {
645
- setIsLoading(false);
646
- }
647
- }, [userId, scope.organisationId, scope.eventId, scope.appId, appName]);
648
-
649
- useEffect(() => {
650
- fetchAccessLevel();
651
- }, [fetchAccessLevel]);
652
-
653
- // Memoize the return object to prevent unnecessary re-renders
654
- return useMemo(() => ({
655
- accessLevel,
656
- isLoading,
657
- error,
658
- refetch: fetchAccessLevel
659
- }), [accessLevel, isLoading, error, fetchAccessLevel]);
660
- }
661
-
662
- /**
663
- * Hook to check multiple permissions at once
664
- *
665
- * @param userId - User ID
666
- * @param scope - Scope for permission checking
667
- * @param permissions - Array of permissions to check
668
- * @param useCache - Whether to use cached results
669
- * @returns Multiple permission check results
670
- *
671
- * @example
672
- * ```tsx
673
- * function MyComponent() {
674
- * const { results, isLoading, error } = useMultiplePermissions(
675
- * userId,
676
- * scope,
677
- * ['read:users', 'create:users', 'update:users']
678
- * );
679
- *
680
- * if (isLoading) return <div>Checking permissions...</div>;
681
- * if (error) return <div>Error: {error.message}</div>;
682
- *
683
- * return (
684
- * <div>
685
- * {results['read:users'] && <UserList />}
686
- * {results['create:users'] && <CreateUserButton />}
687
- * {results['update:users'] && <EditUserButton />}
688
- * </div>
689
- * );
690
- * }
691
- * ```
692
- */
693
- export function useMultiplePermissions(
694
- userId: UUID,
695
- scope: Scope,
696
- permissions: Permission[],
697
- useCache: boolean = true
698
- ): {
699
- results: Record<Permission, boolean>;
700
- isLoading: boolean;
701
- error: Error | null;
702
- refetch: () => Promise<void>;
703
- } {
704
- const [results, setResults] = useState<Record<Permission, boolean>>({} as Record<Permission, boolean>);
705
- const [isLoading, setIsLoading] = useState(true);
706
- const [error, setError] = useState<Error | null>(null);
707
-
708
- const checkPermissions = useCallback(async () => {
709
- if (!userId || permissions.length === 0) {
710
- setResults({} as Record<Permission, boolean>);
711
- setIsLoading(false);
712
- return;
713
- }
714
-
715
- try {
716
- setIsLoading(true);
717
- setError(null);
718
-
719
- const permissionResults: Record<Permission, boolean> = {} as Record<Permission, boolean>;
720
-
721
- // Check each permission
722
- for (const permission of permissions) {
723
- const result = useCache
724
- ? await isPermittedCached({ userId, scope, permission })
725
- : await isPermitted({ userId, scope, permission });
726
- permissionResults[permission] = result;
727
- }
728
-
729
- setResults(permissionResults);
730
- } catch (err) {
731
- setError(err instanceof Error ? err : new Error('Failed to check permissions'));
732
- setResults({} as Record<Permission, boolean>);
733
- } finally {
734
- setIsLoading(false);
735
- }
736
- }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
737
-
738
- useEffect(() => {
739
- checkPermissions();
740
- }, [checkPermissions]);
741
-
742
- // Memoize the return object to prevent unnecessary re-renders
743
- return useMemo(() => ({
744
- results,
745
- isLoading,
746
- error,
747
- refetch: checkPermissions
748
- }), [results, isLoading, error, checkPermissions]);
749
- }
750
-
751
- /**
752
- * Hook to check if user has any of the specified permissions
753
- *
754
- * @param userId - User ID
755
- * @param scope - Scope for permission checking
756
- * @param permissions - Array of permissions to check
757
- * @param useCache - Whether to use cached results
758
- * @returns Whether user has any of the permissions
759
- *
760
- * @example
761
- * ```tsx
762
- * function MyComponent() {
763
- * const { hasAny, isLoading, error } = useHasAnyPermission(
764
- * userId,
765
- * scope,
766
- * ['read:users', 'create:users']
767
- * );
768
- *
769
- * if (isLoading) return <div>Checking permissions...</div>;
770
- * if (error) return <div>Error: {error.message}</div>;
771
- *
772
- * return hasAny ? <UserManagementPanel /> : <div>No user permissions</div>;
773
- * }
774
- * ```
775
- */
776
- export function useHasAnyPermission(
777
- userId: UUID,
778
- scope: Scope,
779
- permissions: Permission[],
780
- useCache: boolean = true
781
- ): {
782
- hasAny: boolean;
783
- isLoading: boolean;
784
- error: Error | null;
785
- refetch: () => Promise<void>;
786
- } {
787
- const [hasAny, setHasAny] = useState<boolean>(false);
788
- const [isLoading, setIsLoading] = useState(true);
789
- const [error, setError] = useState<Error | null>(null);
790
-
791
- const checkAnyPermission = useCallback(async () => {
792
- if (!userId || permissions.length === 0) {
793
- setHasAny(false);
794
- setIsLoading(false);
795
- return;
796
- }
797
-
798
- try {
799
- setIsLoading(true);
800
- setError(null);
801
-
802
- let hasAnyPermission = false;
803
-
804
- for (const permission of permissions) {
805
- const result = useCache
806
- ? await isPermittedCached({ userId, scope, permission })
807
- : await isPermitted({ userId, scope, permission });
808
-
809
- if (result) {
810
- hasAnyPermission = true;
811
- break;
812
- }
813
- }
814
-
815
- setHasAny(hasAnyPermission);
816
- } catch (err) {
817
- setError(err instanceof Error ? err : new Error('Failed to check permissions'));
818
- setHasAny(false);
819
- } finally {
820
- setIsLoading(false);
821
- }
822
- }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
823
-
824
- useEffect(() => {
825
- checkAnyPermission();
826
- }, [checkAnyPermission]);
827
-
828
- // Memoize the return object to prevent unnecessary re-renders
829
- return useMemo(() => ({
830
- hasAny,
831
- isLoading,
832
- error,
833
- refetch: checkAnyPermission
834
- }), [hasAny, isLoading, error, checkAnyPermission]);
835
- }
836
-
837
- /**
838
- * Hook to check if user has all of the specified permissions
839
- *
840
- * @param userId - User ID
841
- * @param scope - Scope for permission checking
842
- * @param permissions - Array of permissions to check
843
- * @param useCache - Whether to use cached results
844
- * @returns Whether user has all of the permissions
845
- *
846
- * @example
847
- * ```tsx
848
- * function MyComponent() {
849
- * const { hasAll, isLoading, error } = useHasAllPermissions(
850
- * userId,
851
- * scope,
852
- * ['read:users', 'create:users', 'update:users']
853
- * );
854
- *
855
- * if (isLoading) return <div>Checking permissions...</div>;
856
- * if (error) return <div>Error: {error.message}</div>;
857
- *
858
- * return hasAll ? <FullUserManagementPanel /> : <div>Insufficient permissions</div>;
859
- * }
860
- * ```
861
- */
862
- export function useHasAllPermissions(
863
- userId: UUID,
864
- scope: Scope,
865
- permissions: Permission[],
866
- useCache: boolean = true
867
- ): {
868
- hasAll: boolean;
869
- isLoading: boolean;
870
- error: Error | null;
871
- refetch: () => Promise<void>;
872
- } {
873
- const [hasAll, setHasAll] = useState<boolean>(false);
874
- const [isLoading, setIsLoading] = useState(true);
875
- const [error, setError] = useState<Error | null>(null);
876
-
877
- const checkAllPermissions = useCallback(async () => {
878
- if (!userId || permissions.length === 0) {
879
- setHasAll(false);
880
- setIsLoading(false);
881
- return;
882
- }
883
-
884
- try {
885
- setIsLoading(true);
886
- setError(null);
887
-
888
- let hasAllPermissions = true;
889
-
890
- for (const permission of permissions) {
891
- const result = useCache
892
- ? await isPermittedCached({ userId, scope, permission })
893
- : await isPermitted({ userId, scope, permission });
894
-
895
- if (!result) {
896
- hasAllPermissions = false;
897
- break;
898
- }
899
- }
900
-
901
- setHasAll(hasAllPermissions);
902
- } catch (err) {
903
- setError(err instanceof Error ? err : new Error('Failed to check permissions'));
904
- setHasAll(false);
905
- } finally {
906
- setIsLoading(false);
907
- }
908
- }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
909
-
910
- useEffect(() => {
911
- checkAllPermissions();
912
- }, [checkAllPermissions]);
913
-
914
- // Memoize the return object to prevent unnecessary re-renders
915
- return useMemo(() => ({
916
- hasAll,
917
- isLoading,
918
- error,
919
- refetch: checkAllPermissions
920
- }), [hasAll, isLoading, error, checkAllPermissions]);
921
- }
922
-
923
- /**
924
- * Hook to get cached permissions with TTL management
925
- *
926
- * @param userId - User ID
927
- * @param scope - Scope for permission checking
928
- * @returns Cached permission state and methods
929
- *
930
- * @example
931
- * ```tsx
932
- * function MyComponent() {
933
- * const { permissions, isLoading, error, invalidateCache } = useCachedPermissions(userId, scope);
934
- *
935
- * if (isLoading) return <div>Loading cached permissions...</div>;
936
- * if (error) return <div>Error: {error.message}</div>;
937
- *
938
- * return (
939
- * <div>
940
- * {permissions['read:users'] && <UserList />}
941
- * <button onClick={invalidateCache}>Refresh Permissions</button>
942
- * </div>
943
- * );
944
- * }
945
- * ```
946
- */
947
- export function useCachedPermissions(userId: UUID, scope: Scope): {
948
- permissions: PermissionMap;
949
- isLoading: boolean;
950
- error: Error | null;
951
- invalidateCache: () => void;
952
- refetch: () => Promise<void>;
953
- } {
954
- const [permissions, setPermissions] = useState<PermissionMap>({} as PermissionMap);
955
- const [isLoading, setIsLoading] = useState(true);
956
- const [error, setError] = useState<Error | null>(null);
957
-
958
- const fetchCachedPermissions = useCallback(async () => {
959
- if (!userId) {
960
- setPermissions({} as PermissionMap);
961
- setIsLoading(false);
962
- return;
963
- }
964
-
965
- try {
966
- setIsLoading(true);
967
- setError(null);
968
-
969
- const permissionMap = await getPermissionMap({ userId, scope });
970
- setPermissions(permissionMap);
971
- } catch (err) {
972
- setError(err instanceof Error ? err : new Error('Failed to fetch cached permissions'));
973
- } finally {
974
- setIsLoading(false);
975
- }
976
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
977
-
978
- const invalidateCache = useCallback(() => {
979
- // This would typically invalidate the cache in the actual implementation
980
- // For now, we'll just refetch
981
- fetchCachedPermissions();
982
- }, [fetchCachedPermissions]);
983
-
984
- useEffect(() => {
985
- fetchCachedPermissions();
986
- }, [fetchCachedPermissions]);
987
-
988
- // Memoize the return object to prevent unnecessary re-renders
989
- return useMemo(() => ({
990
- permissions,
991
- isLoading,
992
- error,
993
- invalidateCache,
994
- refetch: fetchCachedPermissions
995
- }), [permissions, isLoading, error, invalidateCache, fetchCachedPermissions]);
996
- }
2
+ * RBAC Permission Hooks
3
+ *
4
+ * This barrel file re-exports the permission-related hooks. The implementations
5
+ * are organized in `./permissions` to keep the modules focused and maintainable.
6
+ */
7
+ export {
8
+ usePermissions,
9
+ useCan,
10
+ useAccessLevel,
11
+ useMultiplePermissions,
12
+ useHasAnyPermission,
13
+ useHasAllPermissions,
14
+ useCachedPermissions,
15
+ } from './permissions';