@jmruthers/pace-core 0.5.110 → 0.5.112

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 (236) hide show
  1. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  2. package/dist/{DataTable-D3BK2FCN.js → DataTable-3D3BUZDV.js} +8 -8
  3. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-KZZUO27W.js} +3 -3
  4. package/dist/{api-PIE4JRFS.js → api-QPMBZZUZ.js} +5 -3
  5. package/dist/{audit-65VNHEV2.js → audit-H4YJJF7R.js} +2 -2
  6. package/dist/{chunk-Q7APDV6H.js → chunk-3OGQLOJM.js} +23 -7
  7. package/dist/chunk-3OGQLOJM.js.map +1 -0
  8. package/dist/{chunk-EYSXQ756.js → chunk-7H75SHXZ.js} +2 -2
  9. package/dist/{chunk-D6MEKC27.js → chunk-BUN7NMV7.js} +2 -2
  10. package/dist/{chunk-AWK2FAUN.js → chunk-C5RN4TE5.js} +7 -7
  11. package/dist/{chunk-3J5N2T2N.js → chunk-EKVVTPIF.js} +183 -127
  12. package/dist/chunk-EKVVTPIF.js.map +1 -0
  13. package/dist/{chunk-2W4WKJVF.js → chunk-F6QB26OS.js} +290 -255
  14. package/dist/chunk-F6QB26OS.js.map +1 -0
  15. package/dist/{chunk-HADXAZT3.js → chunk-I7JC7PTJ.js} +54 -92
  16. package/dist/chunk-I7JC7PTJ.js.map +1 -0
  17. package/dist/{chunk-EZ64QG2I.js → chunk-L36JW4KV.js} +2 -2
  18. package/dist/{chunk-7GBEBJLR.js → chunk-MNSGWRPB.js} +45 -37
  19. package/dist/chunk-MNSGWRPB.js.map +1 -0
  20. package/dist/{chunk-YFMENCR4.js → chunk-NEONKMTU.js} +3 -3
  21. package/dist/{chunk-AUXS7XSO.js → chunk-OO3V7W4H.js} +35 -11
  22. package/dist/chunk-OO3V7W4H.js.map +1 -0
  23. package/dist/{chunk-XRSP3H52.js → chunk-TAJRS6YB.js} +57 -23
  24. package/dist/chunk-TAJRS6YB.js.map +1 -0
  25. package/dist/{chunk-HGZSO43Y.js → chunk-WMPZY26G.js} +8 -4
  26. package/dist/{chunk-HGZSO43Y.js.map → chunk-WMPZY26G.js.map} +1 -1
  27. package/dist/components.js +10 -10
  28. package/dist/hooks.d.ts +11 -1
  29. package/dist/hooks.js +9 -7
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +13 -13
  33. package/dist/providers.d.ts +2 -2
  34. package/dist/providers.js +2 -2
  35. package/dist/rbac/index.d.ts +13 -8
  36. package/dist/rbac/index.js +9 -9
  37. package/dist/utils.js +1 -1
  38. package/docs/api/classes/ColumnFactory.md +1 -1
  39. package/docs/api/classes/ErrorBoundary.md +1 -1
  40. package/docs/api/classes/InvalidScopeError.md +4 -4
  41. package/docs/api/classes/MissingUserContextError.md +4 -4
  42. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  43. package/docs/api/classes/PermissionDeniedError.md +4 -4
  44. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  45. package/docs/api/classes/RBACAuditManager.md +8 -8
  46. package/docs/api/classes/RBACCache.md +8 -8
  47. package/docs/api/classes/RBACEngine.md +4 -4
  48. package/docs/api/classes/RBACError.md +4 -4
  49. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  50. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  51. package/docs/api/classes/StorageUtils.md +1 -1
  52. package/docs/api/enums/FileCategory.md +1 -1
  53. package/docs/api/interfaces/AggregateConfig.md +1 -1
  54. package/docs/api/interfaces/ButtonProps.md +1 -1
  55. package/docs/api/interfaces/CardProps.md +1 -1
  56. package/docs/api/interfaces/ColorPalette.md +1 -1
  57. package/docs/api/interfaces/ColorShade.md +1 -1
  58. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  59. package/docs/api/interfaces/DataRecord.md +1 -1
  60. package/docs/api/interfaces/DataTableAction.md +1 -1
  61. package/docs/api/interfaces/DataTableColumn.md +1 -1
  62. package/docs/api/interfaces/DataTableProps.md +1 -1
  63. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  64. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  65. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  66. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  67. package/docs/api/interfaces/FileMetadata.md +1 -1
  68. package/docs/api/interfaces/FileReference.md +1 -1
  69. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  70. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  71. package/docs/api/interfaces/FileUploadProps.md +1 -1
  72. package/docs/api/interfaces/FooterProps.md +1 -1
  73. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  74. package/docs/api/interfaces/InputProps.md +1 -1
  75. package/docs/api/interfaces/LabelProps.md +1 -1
  76. package/docs/api/interfaces/LoginFormProps.md +1 -1
  77. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  78. package/docs/api/interfaces/NavigationContextType.md +1 -1
  79. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  80. package/docs/api/interfaces/NavigationItem.md +1 -1
  81. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  82. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  83. package/docs/api/interfaces/Organisation.md +1 -1
  84. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  85. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  86. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  87. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  88. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  89. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  90. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  91. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  92. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  93. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  94. package/docs/api/interfaces/PaletteData.md +1 -1
  95. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  96. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  98. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  99. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  102. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  103. package/docs/api/interfaces/RBACConfig.md +1 -1
  104. package/docs/api/interfaces/RBACLogger.md +1 -1
  105. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  106. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  107. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  108. package/docs/api/interfaces/RouteConfig.md +19 -6
  109. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  110. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  111. package/docs/api/interfaces/StorageConfig.md +1 -1
  112. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  113. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  114. package/docs/api/interfaces/StorageListOptions.md +1 -1
  115. package/docs/api/interfaces/StorageListResult.md +1 -1
  116. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  117. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  118. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  119. package/docs/api/interfaces/StyleImport.md +1 -1
  120. package/docs/api/interfaces/SwitchProps.md +1 -1
  121. package/docs/api/interfaces/ToastActionElement.md +1 -1
  122. package/docs/api/interfaces/ToastProps.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  124. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  126. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  128. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  130. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  133. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  134. package/docs/api/interfaces/UserEventAccess.md +1 -1
  135. package/docs/api/interfaces/UserMenuProps.md +1 -1
  136. package/docs/api/interfaces/UserProfile.md +1 -1
  137. package/docs/api/modules.md +36 -36
  138. package/docs/api-reference/hooks.md +8 -4
  139. package/docs/architecture/rpc-function-standards.md +3 -1
  140. package/docs/best-practices/common-patterns.md +3 -3
  141. package/docs/best-practices/deployment.md +10 -4
  142. package/docs/best-practices/performance.md +11 -3
  143. package/docs/core-concepts/organisations.md +8 -8
  144. package/docs/core-concepts/permissions.md +133 -72
  145. package/docs/migration/rbac-migration.md +65 -66
  146. package/docs/rbac/advanced-patterns.md +15 -22
  147. package/docs/rbac/examples.md +12 -12
  148. package/docs/rbac/getting-started.md +3 -3
  149. package/docs/rbac/troubleshooting.md +2 -1
  150. package/package.json +1 -1
  151. package/src/components/DataTable/DataTable.test.tsx +405 -154
  152. package/src/components/DataTable/components/DataTableCore.tsx +6 -1
  153. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  154. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  155. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  156. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  157. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  158. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  159. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  160. package/src/components/EventSelector/EventSelector.tsx +32 -2
  161. package/src/components/FileUpload/FileUpload.tsx +2 -8
  162. package/src/components/NavigationMenu/NavigationMenu.test.tsx +56 -8
  163. package/src/components/NavigationMenu/NavigationMenu.tsx +75 -12
  164. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  165. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  166. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  167. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  168. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  169. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  170. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  171. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  172. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  173. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  174. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  175. package/src/hooks/index.ts +1 -1
  176. package/src/hooks/useFileDisplay.ts +51 -0
  177. package/src/hooks/usePermissionCache.test.ts +112 -68
  178. package/src/hooks/usePermissionCache.ts +55 -15
  179. package/src/rbac/README.md +81 -39
  180. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  181. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  182. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  183. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  184. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  185. package/src/rbac/adapters.tsx +4 -4
  186. package/src/rbac/api.test.ts +37 -13
  187. package/src/rbac/api.ts +25 -8
  188. package/src/rbac/audit-enhanced.ts +14 -2
  189. package/src/rbac/audit.test.ts +18 -8
  190. package/src/rbac/audit.ts +25 -6
  191. package/src/rbac/cache.test.ts +12 -0
  192. package/src/rbac/cache.ts +29 -9
  193. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  194. package/src/rbac/components/NavigationGuard.tsx +14 -14
  195. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  196. package/src/rbac/components/PagePermissionGuard.tsx +4 -3
  197. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  198. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  199. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  200. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  201. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  202. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  203. package/src/rbac/docs/event-based-apps.md +6 -6
  204. package/src/rbac/engine.ts +12 -2
  205. package/src/rbac/hooks/useCan.test.ts +29 -2
  206. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  207. package/src/rbac/hooks/usePermissions.ts +65 -25
  208. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  209. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  210. package/src/rbac/hooks/useRBAC.ts +0 -55
  211. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  212. package/src/rbac/permissions.test.ts +11 -7
  213. package/src/rbac/security.test.ts +2 -2
  214. package/src/rbac/security.ts +22 -7
  215. package/src/rbac/types.test.ts +2 -2
  216. package/src/rbac/types.ts +1 -2
  217. package/src/services/EventService.ts +42 -13
  218. package/src/services/__tests__/EventService.test.ts +25 -4
  219. package/src/services/interfaces/IEventService.ts +1 -0
  220. package/src/utils/file-reference.ts +9 -0
  221. package/dist/chunk-2W4WKJVF.js.map +0 -1
  222. package/dist/chunk-3J5N2T2N.js.map +0 -1
  223. package/dist/chunk-7GBEBJLR.js.map +0 -1
  224. package/dist/chunk-AUXS7XSO.js.map +0 -1
  225. package/dist/chunk-HADXAZT3.js.map +0 -1
  226. package/dist/chunk-Q7APDV6H.js.map +0 -1
  227. package/dist/chunk-XRSP3H52.js.map +0 -1
  228. /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-3D3BUZDV.js.map} +0 -0
  229. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-KZZUO27W.js.map} +0 -0
  230. /package/dist/{api-PIE4JRFS.js.map → api-QPMBZZUZ.js.map} +0 -0
  231. /package/dist/{audit-65VNHEV2.js.map → audit-H4YJJF7R.js.map} +0 -0
  232. /package/dist/{chunk-EYSXQ756.js.map → chunk-7H75SHXZ.js.map} +0 -0
  233. /package/dist/{chunk-D6MEKC27.js.map → chunk-BUN7NMV7.js.map} +0 -0
  234. /package/dist/{chunk-AWK2FAUN.js.map → chunk-C5RN4TE5.js.map} +0 -0
  235. /package/dist/{chunk-EZ64QG2I.js.map → chunk-L36JW4KV.js.map} +0 -0
  236. /package/dist/{chunk-YFMENCR4.js.map → chunk-NEONKMTU.js.map} +0 -0
@@ -268,8 +268,8 @@ describe('usePermissions Hook', () => {
268
268
  'create:users': true,
269
269
  'update:users': true,
270
270
  'delete:users': true,
271
- 'manage:organisations': true,
272
- 'manage:events': true,
271
+ 'update:organisations': true,
272
+ 'update:events': true,
273
273
  'super_admin': true
274
274
  };
275
275
 
@@ -287,7 +287,7 @@ describe('usePermissions Hook', () => {
287
287
  expect(result.current.hasPermission('create:users')).toBe(true);
288
288
  expect(result.current.hasPermission('update:users')).toBe(true);
289
289
  expect(result.current.hasPermission('delete:users')).toBe(true);
290
- expect(result.current.hasPermission('manage:organisations')).toBe(true);
290
+ expect(result.current.hasPermission('update:organisations')).toBe(true);
291
291
  expect(result.current.hasPermission('super_admin')).toBe(true);
292
292
  });
293
293
 
@@ -297,8 +297,8 @@ describe('usePermissions Hook', () => {
297
297
  'create:users': true,
298
298
  'update:users': true,
299
299
  'delete:users': false, // Org admin can't delete users
300
- 'manage:organisations': true,
301
- 'manage:events': true,
300
+ 'update:organisations': true,
301
+ 'update:events': true,
302
302
  'org_admin': true
303
303
  };
304
304
 
@@ -316,7 +316,7 @@ describe('usePermissions Hook', () => {
316
316
  expect(result.current.hasPermission('create:users')).toBe(true);
317
317
  expect(result.current.hasPermission('update:users')).toBe(true);
318
318
  expect(result.current.hasPermission('delete:users')).toBe(false);
319
- expect(result.current.hasPermission('manage:organisations')).toBe(true);
319
+ expect(result.current.hasPermission('update:organisations')).toBe(true);
320
320
  expect(result.current.hasPermission('org_admin')).toBe(true);
321
321
  });
322
322
 
@@ -326,8 +326,8 @@ describe('usePermissions Hook', () => {
326
326
  'create:users': false,
327
327
  'update:users': false,
328
328
  'delete:users': false,
329
- 'manage:organisations': false,
330
- 'manage:events': true,
329
+ 'update:organisations': false,
330
+ 'update:events': true,
331
331
  'event_admin': true
332
332
  };
333
333
 
@@ -345,8 +345,8 @@ describe('usePermissions Hook', () => {
345
345
  expect(result.current.hasPermission('create:users')).toBe(false);
346
346
  expect(result.current.hasPermission('update:users')).toBe(false);
347
347
  expect(result.current.hasPermission('delete:users')).toBe(false);
348
- expect(result.current.hasPermission('manage:organisations')).toBe(false);
349
- expect(result.current.hasPermission('manage:events')).toBe(true);
348
+ expect(result.current.hasPermission('update:organisations')).toBe(false);
349
+ expect(result.current.hasPermission('update:events')).toBe(true);
350
350
  expect(result.current.hasPermission('event_admin')).toBe(true);
351
351
  });
352
352
 
@@ -356,8 +356,8 @@ describe('usePermissions Hook', () => {
356
356
  'create:users': false,
357
357
  'update:users': false,
358
358
  'delete:users': false,
359
- 'manage:organisations': false,
360
- 'manage:events': false,
359
+ 'update:organisations': false,
360
+ 'update:events': false,
361
361
  'member': true
362
362
  };
363
363
 
@@ -375,8 +375,8 @@ describe('usePermissions Hook', () => {
375
375
  expect(result.current.hasPermission('create:users')).toBe(false);
376
376
  expect(result.current.hasPermission('update:users')).toBe(false);
377
377
  expect(result.current.hasPermission('delete:users')).toBe(false);
378
- expect(result.current.hasPermission('manage:organisations')).toBe(false);
379
- expect(result.current.hasPermission('manage:events')).toBe(false);
378
+ expect(result.current.hasPermission('update:organisations')).toBe(false);
379
+ expect(result.current.hasPermission('update:events')).toBe(false);
380
380
  expect(result.current.hasPermission('member')).toBe(true);
381
381
  });
382
382
 
@@ -386,8 +386,8 @@ describe('usePermissions Hook', () => {
386
386
  'create:users': false,
387
387
  'update:users': false,
388
388
  'delete:users': false,
389
- 'manage:organisations': false,
390
- 'manage:events': false,
389
+ 'update:organisations': false,
390
+ 'update:events': false,
391
391
  'participant': true
392
392
  };
393
393
 
@@ -405,8 +405,8 @@ describe('usePermissions Hook', () => {
405
405
  expect(result.current.hasPermission('create:users')).toBe(false);
406
406
  expect(result.current.hasPermission('update:users')).toBe(false);
407
407
  expect(result.current.hasPermission('delete:users')).toBe(false);
408
- expect(result.current.hasPermission('manage:organisations')).toBe(false);
409
- expect(result.current.hasPermission('manage:events')).toBe(false);
408
+ expect(result.current.hasPermission('update:organisations')).toBe(false);
409
+ expect(result.current.hasPermission('update:events')).toBe(false);
410
410
  expect(result.current.hasPermission('participant')).toBe(true);
411
411
  });
412
412
 
@@ -416,8 +416,8 @@ describe('usePermissions Hook', () => {
416
416
  'create:users': false,
417
417
  'update:users': true,
418
418
  'delete:users': false,
419
- 'manage:organisations': false,
420
- 'manage:events': true
419
+ 'update:organisations': false,
420
+ 'update:events': true
421
421
  };
422
422
 
423
423
  mockGetPermissionMap.mockResolvedValue(mixedPermissions);
@@ -432,8 +432,8 @@ describe('usePermissions Hook', () => {
432
432
  // Test hasAnyPermission with mixed results
433
433
  expect(result.current.hasAnyPermission(['read:users', 'create:users'])).toBe(true); // One is true
434
434
  expect(result.current.hasAnyPermission(['create:users', 'delete:users'])).toBe(false); // Both are false
435
- expect(result.current.hasAnyPermission(['update:users', 'manage:events'])).toBe(true); // Both are true
436
- expect(result.current.hasAnyPermission(['read:users', 'update:users', 'manage:events'])).toBe(true); // All are true
435
+ expect(result.current.hasAnyPermission(['update:users', 'update:events'])).toBe(true); // Both are true
436
+ expect(result.current.hasAnyPermission(['read:users', 'update:users', 'update:events'])).toBe(true); // All are true
437
437
  });
438
438
 
439
439
  it('handles mixed permission scenarios with hasAllPermissions', async () => {
@@ -442,8 +442,8 @@ describe('usePermissions Hook', () => {
442
442
  'create:users': false,
443
443
  'update:users': true,
444
444
  'delete:users': false,
445
- 'manage:organisations': false,
446
- 'manage:events': true
445
+ 'update:organisations': false,
446
+ 'update:events': true
447
447
  };
448
448
 
449
449
  mockGetPermissionMap.mockResolvedValue(mixedPermissions);
@@ -459,7 +459,7 @@ describe('usePermissions Hook', () => {
459
459
  expect(result.current.hasAllPermissions(['read:users', 'update:users'])).toBe(true); // Both are true
460
460
  expect(result.current.hasAllPermissions(['read:users', 'create:users'])).toBe(false); // One is false
461
461
  expect(result.current.hasAllPermissions(['create:users', 'delete:users'])).toBe(false); // Both are false
462
- expect(result.current.hasAllPermissions(['read:users', 'update:users', 'manage:events'])).toBe(true); // All are true
462
+ expect(result.current.hasAllPermissions(['read:users', 'update:users', 'update:events'])).toBe(true); // All are true
463
463
  });
464
464
  });
465
465
 
@@ -52,6 +52,22 @@ export function usePermissions(userId: UUID, scope: Scope) {
52
52
  const [error, setError] = useState<Error | null>(null);
53
53
  const isFetchingRef = useRef(false);
54
54
 
55
+ // Add timeout for missing organisation context (3 seconds)
56
+ useEffect(() => {
57
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
58
+ const timeoutId = setTimeout(() => {
59
+ setError(new Error('Organisation context is required for permission checks'));
60
+ setIsLoading(false);
61
+ }, 3000); // 3 seconds - typical permission check is < 1 second
62
+
63
+ return () => clearTimeout(timeoutId);
64
+ }
65
+ // Clear error if organisation context becomes available
66
+ if (error?.message === 'Organisation context is required for permission checks') {
67
+ setError(null);
68
+ }
69
+ }, [scope.organisationId, error]);
70
+
55
71
  useEffect(() => {
56
72
  const fetchPermissions = async () => {
57
73
  // Prevent multiple simultaneous fetches
@@ -65,10 +81,11 @@ export function usePermissions(userId: UUID, scope: Scope) {
65
81
  return;
66
82
  }
67
83
 
68
- // Don't fetch permissions if scope is invalid (e.g., organisationId is empty)
69
- // Return empty permissions and loading true for invalid scopes to prevent premature access denied
70
- if (!scope.organisationId || scope.organisationId.trim() === '') {
71
- setPermissions({} as PermissionMap);
84
+ // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
85
+ // Wait for organisation context to resolve
86
+ // IMPORTANT: Don't clear existing permissions here - keep them until we have new ones
87
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
88
+ // Keep existing permissions, just mark as loading
72
89
  setIsLoading(true);
73
90
  setError(null);
74
91
  return;
@@ -79,10 +96,17 @@ export function usePermissions(userId: UUID, scope: Scope) {
79
96
  setIsLoading(true);
80
97
  setError(null);
81
98
 
99
+ // Fetch new permissions - don't clear old ones until we have new ones
82
100
  const permissionMap = await getPermissionMap({ userId, scope });
101
+
102
+ // Only update permissions if fetch was successful
83
103
  setPermissions(permissionMap);
84
104
  } catch (err) {
105
+ // On error, keep existing permissions but set error state
106
+ // This prevents the UI from losing all items when there's a transient error
107
+ console.error('[usePermissions] Failed to fetch permissions:', err);
85
108
  setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
109
+ // Don't clear permissions on error - keep what we had
86
110
  } finally {
87
111
  setIsLoading(false);
88
112
  isFetchingRef.current = false;
@@ -125,9 +149,10 @@ export function usePermissions(userId: UUID, scope: Scope) {
125
149
  return;
126
150
  }
127
151
 
128
- // Don't fetch permissions if scope is invalid (e.g., organisationId is empty)
129
- if (!scope.organisationId || scope.organisationId.trim() === '') {
130
- setPermissions({} as PermissionMap);
152
+ // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
153
+ // IMPORTANT: Don't clear existing permissions - keep them until we have new ones
154
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
155
+ // Keep existing permissions, just mark as loading
131
156
  setIsLoading(true);
132
157
  setError(null);
133
158
  return;
@@ -138,10 +163,17 @@ export function usePermissions(userId: UUID, scope: Scope) {
138
163
  setIsLoading(true);
139
164
  setError(null);
140
165
 
166
+ // Fetch new permissions - don't clear old ones until we have new ones
141
167
  const permissionMap = await getPermissionMap({ userId, scope });
168
+
169
+ // Only update permissions if fetch was successful
142
170
  setPermissions(permissionMap);
143
171
  } catch (err) {
172
+ // On error, keep existing permissions but set error state
173
+ // This prevents the UI from losing all items when there's a transient error
174
+ console.error('[usePermissions] Failed to refetch permissions:', err);
144
175
  setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
176
+ // Don't clear permissions on error - keep what we had
145
177
  } finally {
146
178
  setIsLoading(false);
147
179
  isFetchingRef.current = false;
@@ -193,6 +225,23 @@ export function useCan(
193
225
  const [isLoading, setIsLoading] = useState(true);
194
226
  const [error, setError] = useState<Error | null>(null);
195
227
 
228
+ // Add timeout for missing organisation context (3 seconds)
229
+ useEffect(() => {
230
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
231
+ const timeoutId = setTimeout(() => {
232
+ setError(new Error('Organisation context is required for permission checks'));
233
+ setIsLoading(false);
234
+ setCan(false);
235
+ }, 3000); // 3 seconds - typical permission check is < 1 second
236
+
237
+ return () => clearTimeout(timeoutId);
238
+ }
239
+ // Clear error if organisation context becomes available
240
+ if (error?.message === 'Organisation context is required for permission checks') {
241
+ setError(null);
242
+ }
243
+ }, [scope.organisationId, error]);
244
+
196
245
  // Use refs to track the last values to prevent unnecessary re-runs
197
246
  const lastUserIdRef = useRef<UUID | null>(null);
198
247
  const lastScopeRef = useRef<string | null>(null);
@@ -226,12 +275,13 @@ export function useCan(
226
275
  return;
227
276
  }
228
277
 
229
- // Don't check permissions if scope is invalid (e.g., organisationId is empty)
230
- // Return can: false and loading: true for invalid scopes to prevent premature access denied
231
- if (!scope.organisationId || scope.organisationId.trim() === '') {
232
- console.log('[useCan] Invalid scope - organisationId is empty:', { scope, permission, pageId });
233
- setCan(false);
278
+ // Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
279
+ // Wait for organisation context to resolve
280
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
234
281
  setIsLoading(true);
282
+ setCan(false);
283
+ setError(null);
284
+ // Timeout is handled in separate useEffect (Phase 1.4)
235
285
  return;
236
286
  }
237
287
 
@@ -239,21 +289,10 @@ export function useCan(
239
289
  setIsLoading(true);
240
290
  setError(null);
241
291
 
242
- console.log('[useCan] Checking permission:', {
243
- userId,
244
- organisationId: scope.organisationId,
245
- eventId: scope.eventId,
246
- appId: scope.appId,
247
- permission,
248
- pageId
249
- });
250
-
251
292
  const result = useCache
252
293
  ? await isPermittedCached({ userId, scope, permission, pageId })
253
294
  : await isPermitted({ userId, scope, permission, pageId });
254
295
 
255
- console.log('[useCan] Permission check result:', { permission, result, pageId });
256
-
257
296
  setCan(result);
258
297
  } catch (err) {
259
298
  console.error('[useCan] Permission check error:', { permission, error: err });
@@ -275,10 +314,11 @@ export function useCan(
275
314
  return;
276
315
  }
277
316
 
278
- // Don't check permissions if scope is invalid (e.g., organisationId is empty)
279
- if (!scope.organisationId || scope.organisationId.trim() === '') {
317
+ // Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
318
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
280
319
  setCan(false);
281
320
  setIsLoading(true);
321
+ setError(null);
282
322
  return;
283
323
  }
284
324
 
@@ -57,7 +57,6 @@ vi.mock('./useRBAC', () => ({
57
57
  eventAppRole: 'participant',
58
58
  isSuperAdmin: false,
59
59
  isOrgAdmin: false,
60
- hasPermission: () => true,
61
60
  hasGlobalPermission: () => true,
62
61
  error: null,
63
62
  }),
@@ -80,18 +79,12 @@ describe('useRBAC Hook - Simple Tests', () => {
80
79
  expect(result.current).toHaveProperty('globalRole');
81
80
  expect(result.current).toHaveProperty('organisationRole');
82
81
  expect(result.current).toHaveProperty('eventAppRole');
83
- expect(result.current).toHaveProperty('hasPermission');
84
82
  expect(result.current).toHaveProperty('hasGlobalPermission');
85
83
  expect(result.current).toHaveProperty('isSuperAdmin');
86
84
  expect(result.current).toHaveProperty('isOrgAdmin');
87
85
  expect(result.current).toHaveProperty('isLoading');
88
86
  expect(result.current).toHaveProperty('error');
89
- });
90
-
91
- it('hasPermission is a function', () => {
92
- const { result } = renderHook(() => useRBAC());
93
-
94
- expect(typeof result.current.hasPermission).toBe('function');
87
+ // Note: hasPermission was removed - use useCan() hook instead
95
88
  });
96
89
 
97
90
  it('hasGlobalPermission is a function', () => {
@@ -97,7 +97,7 @@ describe('useRBAC', () => {
97
97
 
98
98
  await waitFor(() => {
99
99
  expect(result.current.isLoading).toBe(false);
100
- expect(result.current.hasPermission).toBeTypeOf('function');
100
+ expect(result.current.hasGlobalPermission).toBeTypeOf('function');
101
101
  });
102
102
 
103
103
  expect(mockResolveAppContext).toHaveBeenCalledWith({ userId: 'user-1', appName: 'test-app' });
@@ -109,45 +109,8 @@ describe('useRBAC', () => {
109
109
  );
110
110
  });
111
111
 
112
- it('uses cached permission map before hitting engine', async () => {
113
- mockUseUnifiedAuth.mockReturnValue({
114
- user: { id: 'user-1' },
115
- session: { access_token: 'token' },
116
- appName: 'test-app',
117
- });
118
- mockUseOrganisations.mockReturnValue({ selectedOrganisation: { id: 'org-1' } });
119
- mockGetPermissionMap.mockResolvedValue({ 'read:dashboard': true });
120
-
121
- const { result } = renderHook(() => useRBAC('dashboard'));
122
-
123
- await waitFor(() => expect(result.current.isLoading).toBe(false));
124
-
125
- mockIsPermittedCached.mockClear();
126
- const allowed = await result.current.hasPermission('read', 'dashboard');
127
-
128
- expect(allowed).toBe(true);
129
- expect(mockIsPermittedCached).not.toHaveBeenCalled();
130
- });
131
-
132
- it('falls back to engine when permission not cached', async () => {
133
- mockUseUnifiedAuth.mockReturnValue({
134
- user: { id: 'user-1' },
135
- session: { access_token: 'token' },
136
- appName: 'test-app',
137
- });
138
- mockUseOrganisations.mockReturnValue({ selectedOrganisation: { id: 'org-1' } });
139
- mockGetPermissionMap.mockResolvedValue({});
140
- mockIsPermittedCached.mockResolvedValue(true);
141
-
142
- const { result } = renderHook(() => useRBAC('dashboard'));
143
-
144
- await waitFor(() => expect(result.current.isLoading).toBe(false));
145
-
146
- const allowed = await result.current.hasPermission('read', 'dashboard');
147
-
148
- expect(allowed).toBe(true);
149
- expect(mockIsPermittedCached).toHaveBeenCalled();
150
- });
112
+ // Note: Permission checking tests have been moved to useCan.test.ts
113
+ // useRBAC now focuses on role information and context, not permission checks
151
114
 
152
115
  it('handles denied app resolution securely', async () => {
153
116
  mockUseUnifiedAuth.mockReturnValue({
@@ -17,7 +17,6 @@ import { useEvents } from '../../hooks/useEvents';
17
17
  import {
18
18
  getPermissionMap,
19
19
  getAccessLevel,
20
- isPermittedCached,
21
20
  resolveAppContext,
22
21
  getRoleContext,
23
22
  } from '../api';
@@ -27,7 +26,6 @@ import type {
27
26
  GlobalRole,
28
27
  OrganisationRole,
29
28
  EventAppRole,
30
- Operation,
31
29
  Permission,
32
30
  Scope,
33
31
  PermissionMap,
@@ -125,58 +123,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
125
123
  }
126
124
  }, [appName, logger, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user]);
127
125
 
128
- const hasPermission = useCallback(
129
- async (operationOrPermission: Operation | string, targetPageId?: string): Promise<boolean> => {
130
- if (!user) {
131
- logger.warn('[useRBAC] Permission check attempted without authenticated user context');
132
- return false;
133
- }
134
-
135
- if (!currentScope || !currentScope.organisationId) {
136
- logger.error('[useRBAC] Permission check denied due to missing organisation context', {
137
- hasScope: !!currentScope,
138
- scope: currentScope,
139
- userId: user.id,
140
- permission: operationOrPermission,
141
- });
142
- return false;
143
- }
144
-
145
- if (globalRole === 'super_admin' || permissionMap['*']) {
146
- logger.info('[useRBAC] Super admin bypass granted', {
147
- userId: user.id,
148
- permission: operationOrPermission,
149
- });
150
- return true;
151
- }
152
-
153
- let permission: Permission;
154
- if (operationOrPermission.includes(':')) {
155
- permission = operationOrPermission as Permission;
156
- } else if (targetPageId || pageId) {
157
- permission = `${operationOrPermission}:${targetPageId || pageId}` as Permission;
158
- } else {
159
- permission = operationOrPermission as Permission;
160
- }
161
-
162
- const cachedValue = permissionMap[permission];
163
- if (cachedValue === true) {
164
- return true;
165
- }
166
- if (cachedValue === false) {
167
- return false;
168
- }
169
-
170
- return isPermittedCached({
171
- userId: user.id as UUID,
172
- scope: currentScope,
173
- permission,
174
- pageId: targetPageId ?? pageId,
175
- });
176
- },
177
- [currentScope, globalRole, logger, pageId, permissionMap, user],
178
- );
179
-
180
126
  const hasGlobalPermission = useCallback(
181
127
  (permission: string): boolean => {
182
128
  if (globalRole === 'super_admin' || permissionMap['*']) {
@@ -211,7 +157,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
211
157
  globalRole,
212
158
  organisationRole,
213
159
  eventAppRole,
214
- hasPermission,
215
160
  hasGlobalPermission,
216
161
  isSuperAdmin,
217
162
  isOrgAdmin,
@@ -74,28 +74,30 @@ export function useResolvedScope({
74
74
  eventId: undefined
75
75
  });
76
76
 
77
- // Only update the stable scope if the resolved scope has actually changed
78
- if (resolvedScope && resolvedScope.organisationId) {
79
- const newScope = {
80
- organisationId: resolvedScope.organisationId,
81
- appId: resolvedScope.appId,
82
- eventId: resolvedScope.eventId
83
- };
84
-
85
- // Only update if the scope has actually changed
86
- if (stableScopeRef.current.organisationId !== newScope.organisationId ||
87
- stableScopeRef.current.eventId !== newScope.eventId ||
88
- stableScopeRef.current.appId !== newScope.appId) {
89
- stableScopeRef.current = {
90
- organisationId: newScope.organisationId,
91
- appId: newScope.appId || '',
92
- eventId: newScope.eventId
77
+ // Update stable scope ref in useEffect to avoid updates during render
78
+ useEffect(() => {
79
+ if (resolvedScope && resolvedScope.organisationId) {
80
+ const newScope = {
81
+ organisationId: resolvedScope.organisationId,
82
+ appId: resolvedScope.appId,
83
+ eventId: resolvedScope.eventId
93
84
  };
85
+
86
+ // Only update if the scope has actually changed
87
+ if (stableScopeRef.current.organisationId !== newScope.organisationId ||
88
+ stableScopeRef.current.eventId !== newScope.eventId ||
89
+ stableScopeRef.current.appId !== newScope.appId) {
90
+ stableScopeRef.current = {
91
+ organisationId: newScope.organisationId,
92
+ appId: newScope.appId || '',
93
+ eventId: newScope.eventId
94
+ };
95
+ }
96
+ } else if (!resolvedScope) {
97
+ // Reset to empty scope when no resolved scope
98
+ stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
94
99
  }
95
- } else if (!resolvedScope) {
96
- // Reset to empty scope when no resolved scope
97
- stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
98
- }
100
+ }, [resolvedScope]);
99
101
 
100
102
  const stableScope = stableScopeRef.current;
101
103
 
@@ -144,17 +146,10 @@ export function useResolvedScope({
144
146
  }
145
147
  }
146
148
 
147
- // Debug logging
148
- console.log('[useResolvedScope] Attempting to resolve scope:', {
149
- selectedOrganisationId,
150
- selectedEventId,
151
- appId: appId || 'NOT RESOLVED YET',
152
- resolveStep: 'initial'
153
- });
149
+ // Resolve scope based on available context
154
150
 
155
151
  // If we have both organisation and event, use them directly
156
152
  if (selectedOrganisationId && selectedEventId) {
157
- console.log('[useResolvedScope] Resolving with both org and event');
158
153
  if (!cancelled) {
159
154
  setResolvedScope({
160
155
  organisationId: selectedOrganisationId,
@@ -168,7 +163,6 @@ export function useResolvedScope({
168
163
 
169
164
  // If we only have organisation, use it
170
165
  if (selectedOrganisationId) {
171
- console.log('[useResolvedScope] Resolving with organisation only');
172
166
  if (!cancelled) {
173
167
  setResolvedScope({
174
168
  organisationId: selectedOrganisationId,
@@ -183,7 +177,6 @@ export function useResolvedScope({
183
177
  // If we only have event, resolve organisation from event
184
178
  if (selectedEventId && supabase) {
185
179
  try {
186
- console.log('[useResolvedScope] Resolving from event:', { selectedEventId, appId });
187
180
  const eventScope = await createScopeFromEvent(supabase, selectedEventId, appId);
188
181
  if (!eventScope) {
189
182
  console.error('[useResolvedScope] Could not resolve organization from event context');
@@ -194,7 +187,6 @@ export function useResolvedScope({
194
187
  }
195
188
  return;
196
189
  }
197
- console.log('[useResolvedScope] Resolved from event:', eventScope);
198
190
  // Preserve the resolved app ID
199
191
  if (!cancelled) {
200
192
  setResolvedScope({
@@ -84,13 +84,17 @@ describe('RBAC Permissions', () => {
84
84
 
85
85
  it('rejects invalid Permission types', () => {
86
86
  const invalidPermissions = [
87
- 'manage:users',
88
- 'READ:users',
89
- 'read:',
90
- ':users',
91
- 'read:users*',
92
- 'read:*users',
93
- 'invalid'
87
+ 'READ:users', // Capital letters not allowed
88
+ 'read:', // Missing resource
89
+ ':users', // Missing operation
90
+ 'read:users*', // Invalid wildcard placement
91
+ 'read:*users', // Invalid wildcard placement
92
+ 'invalid', // Not in format operation:resource
93
+ 'read:users-', // Invalid character (hyphen) at end
94
+ 'read:users_', // Invalid character (underscore) at end
95
+ 'read:.users', // Cannot start with dot
96
+ 'read:users.', // Cannot end with dot
97
+ 'read:users..detail', // Cannot have consecutive dots
94
98
  ];
95
99
 
96
100
  invalidPermissions.forEach(permission => {
@@ -23,7 +23,7 @@ describe('RBACSecurityValidator', () => {
23
23
  'create:user-profiles',
24
24
  'update:events',
25
25
  'delete:organisations',
26
- 'manage:base.events',
26
+ 'update:base.events',
27
27
  'read:user-profiles.details'
28
28
  ];
29
29
 
@@ -352,7 +352,7 @@ describe('RBACSecurityMiddleware', () => {
352
352
  const input = {
353
353
  userId: '123e4567-e89b-12d3-a456-426614174000' as UUID,
354
354
  scope: { organisationId: '123e4567-e89b-12d3-a456-426614174001' as UUID },
355
- permission: 'manage:everything' as Permission
355
+ permission: 'update:everything' as Permission
356
356
  };
357
357
 
358
358
  const context = {