@jmruthers/pace-core 0.5.110 → 0.5.111

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 (230) hide show
  1. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  2. package/dist/{DataTable-D3BK2FCN.js → DataTable-5W2HVLLV.js} +8 -8
  3. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
  4. package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
  5. package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
  6. package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
  7. package/dist/chunk-2BIDKXQU.js.map +1 -0
  8. package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
  9. package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
  10. package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
  11. package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
  12. package/dist/chunk-IWJYNWXN.js.map +1 -0
  13. package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
  14. package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
  15. package/dist/chunk-MW73E7SP.js.map +1 -0
  16. package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
  17. package/dist/chunk-PXXS26G5.js.map +1 -0
  18. package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
  19. package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
  20. package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
  21. package/dist/chunk-UGVU7L7N.js.map +1 -0
  22. package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
  23. package/dist/chunk-X7SPKHYZ.js.map +1 -0
  24. package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
  25. package/dist/chunk-ZL45MG76.js.map +1 -0
  26. package/dist/components.js +10 -10
  27. package/dist/hooks.d.ts +11 -1
  28. package/dist/hooks.js +9 -7
  29. package/dist/hooks.js.map +1 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +13 -13
  32. package/dist/providers.d.ts +2 -2
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +13 -8
  35. package/dist/rbac/index.js +9 -9
  36. package/dist/utils.js +1 -1
  37. package/docs/api/classes/ColumnFactory.md +1 -1
  38. package/docs/api/classes/ErrorBoundary.md +1 -1
  39. package/docs/api/classes/InvalidScopeError.md +4 -4
  40. package/docs/api/classes/MissingUserContextError.md +4 -4
  41. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  42. package/docs/api/classes/PermissionDeniedError.md +4 -4
  43. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  44. package/docs/api/classes/RBACAuditManager.md +8 -8
  45. package/docs/api/classes/RBACCache.md +8 -8
  46. package/docs/api/classes/RBACEngine.md +4 -4
  47. package/docs/api/classes/RBACError.md +4 -4
  48. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  49. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  50. package/docs/api/classes/StorageUtils.md +1 -1
  51. package/docs/api/enums/FileCategory.md +1 -1
  52. package/docs/api/interfaces/AggregateConfig.md +1 -1
  53. package/docs/api/interfaces/ButtonProps.md +1 -1
  54. package/docs/api/interfaces/CardProps.md +1 -1
  55. package/docs/api/interfaces/ColorPalette.md +1 -1
  56. package/docs/api/interfaces/ColorShade.md +1 -1
  57. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  58. package/docs/api/interfaces/DataRecord.md +1 -1
  59. package/docs/api/interfaces/DataTableAction.md +1 -1
  60. package/docs/api/interfaces/DataTableColumn.md +1 -1
  61. package/docs/api/interfaces/DataTableProps.md +1 -1
  62. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  63. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  64. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  65. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  66. package/docs/api/interfaces/FileMetadata.md +1 -1
  67. package/docs/api/interfaces/FileReference.md +1 -1
  68. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  69. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  70. package/docs/api/interfaces/FileUploadProps.md +1 -1
  71. package/docs/api/interfaces/FooterProps.md +1 -1
  72. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  73. package/docs/api/interfaces/InputProps.md +1 -1
  74. package/docs/api/interfaces/LabelProps.md +1 -1
  75. package/docs/api/interfaces/LoginFormProps.md +1 -1
  76. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  77. package/docs/api/interfaces/NavigationContextType.md +1 -1
  78. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  79. package/docs/api/interfaces/NavigationItem.md +1 -1
  80. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  81. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  82. package/docs/api/interfaces/Organisation.md +1 -1
  83. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  84. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  85. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  86. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  87. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  88. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  89. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  90. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  91. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  92. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  93. package/docs/api/interfaces/PaletteData.md +1 -1
  94. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  95. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  96. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  98. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  99. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  102. package/docs/api/interfaces/RBACConfig.md +1 -1
  103. package/docs/api/interfaces/RBACLogger.md +1 -1
  104. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  105. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  106. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  107. package/docs/api/interfaces/RouteConfig.md +19 -6
  108. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  109. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  110. package/docs/api/interfaces/StorageConfig.md +1 -1
  111. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  112. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  113. package/docs/api/interfaces/StorageListOptions.md +1 -1
  114. package/docs/api/interfaces/StorageListResult.md +1 -1
  115. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  116. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  117. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  118. package/docs/api/interfaces/StyleImport.md +1 -1
  119. package/docs/api/interfaces/SwitchProps.md +1 -1
  120. package/docs/api/interfaces/ToastActionElement.md +1 -1
  121. package/docs/api/interfaces/ToastProps.md +1 -1
  122. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  128. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  130. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  133. package/docs/api/interfaces/UserEventAccess.md +1 -1
  134. package/docs/api/interfaces/UserMenuProps.md +1 -1
  135. package/docs/api/interfaces/UserProfile.md +1 -1
  136. package/docs/api/modules.md +36 -36
  137. package/docs/api-reference/hooks.md +8 -4
  138. package/docs/architecture/rpc-function-standards.md +3 -1
  139. package/docs/best-practices/common-patterns.md +3 -3
  140. package/docs/best-practices/deployment.md +10 -4
  141. package/docs/best-practices/performance.md +11 -3
  142. package/docs/core-concepts/organisations.md +8 -8
  143. package/docs/core-concepts/permissions.md +133 -72
  144. package/docs/migration/rbac-migration.md +65 -66
  145. package/docs/rbac/advanced-patterns.md +15 -22
  146. package/docs/rbac/examples.md +12 -12
  147. package/docs/rbac/getting-started.md +3 -3
  148. package/docs/rbac/troubleshooting.md +2 -1
  149. package/package.json +1 -1
  150. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  151. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  152. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  153. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  154. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  155. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  156. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  157. package/src/components/FileUpload/FileUpload.tsx +2 -8
  158. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  159. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  160. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  161. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  162. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  163. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  164. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  165. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  166. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  167. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  168. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  169. package/src/hooks/index.ts +1 -1
  170. package/src/hooks/useFileDisplay.ts +51 -0
  171. package/src/hooks/usePermissionCache.test.ts +112 -68
  172. package/src/hooks/usePermissionCache.ts +55 -15
  173. package/src/rbac/README.md +81 -39
  174. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  175. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  176. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  177. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  178. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  179. package/src/rbac/adapters.tsx +4 -4
  180. package/src/rbac/api.test.ts +37 -13
  181. package/src/rbac/api.ts +25 -8
  182. package/src/rbac/audit.test.ts +2 -2
  183. package/src/rbac/audit.ts +14 -5
  184. package/src/rbac/cache.test.ts +12 -0
  185. package/src/rbac/cache.ts +29 -9
  186. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  187. package/src/rbac/components/NavigationGuard.tsx +14 -14
  188. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  189. package/src/rbac/components/PagePermissionGuard.tsx +4 -3
  190. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  191. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  192. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  193. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  194. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  195. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  196. package/src/rbac/docs/event-based-apps.md +6 -6
  197. package/src/rbac/engine.ts +12 -2
  198. package/src/rbac/hooks/useCan.test.ts +29 -2
  199. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  200. package/src/rbac/hooks/usePermissions.ts +47 -23
  201. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  202. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  203. package/src/rbac/hooks/useRBAC.ts +0 -55
  204. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  205. package/src/rbac/permissions.test.ts +11 -7
  206. package/src/rbac/security.test.ts +2 -2
  207. package/src/rbac/security.ts +22 -7
  208. package/src/rbac/types.test.ts +2 -2
  209. package/src/rbac/types.ts +1 -2
  210. package/src/services/EventService.ts +41 -13
  211. package/src/services/__tests__/EventService.test.ts +25 -4
  212. package/src/services/interfaces/IEventService.ts +1 -0
  213. package/src/utils/file-reference.ts +9 -0
  214. package/dist/chunk-2W4WKJVF.js.map +0 -1
  215. package/dist/chunk-3J5N2T2N.js.map +0 -1
  216. package/dist/chunk-7GBEBJLR.js.map +0 -1
  217. package/dist/chunk-AUXS7XSO.js.map +0 -1
  218. package/dist/chunk-HADXAZT3.js.map +0 -1
  219. package/dist/chunk-Q7APDV6H.js.map +0 -1
  220. package/dist/chunk-XRSP3H52.js.map +0 -1
  221. /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-5W2HVLLV.js.map} +0 -0
  222. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
  223. /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
  224. /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
  225. /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
  226. /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
  227. /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
  228. /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
  229. /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
  230. /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.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,9 +81,9 @@ 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() === '') {
84
+ // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
85
+ // Wait for organisation context to resolve
86
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
71
87
  setPermissions({} as PermissionMap);
72
88
  setIsLoading(true);
73
89
  setError(null);
@@ -125,8 +141,8 @@ export function usePermissions(userId: UUID, scope: Scope) {
125
141
  return;
126
142
  }
127
143
 
128
- // Don't fetch permissions if scope is invalid (e.g., organisationId is empty)
129
- if (!scope.organisationId || scope.organisationId.trim() === '') {
144
+ // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
145
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
130
146
  setPermissions({} as PermissionMap);
131
147
  setIsLoading(true);
132
148
  setError(null);
@@ -193,6 +209,23 @@ export function useCan(
193
209
  const [isLoading, setIsLoading] = useState(true);
194
210
  const [error, setError] = useState<Error | null>(null);
195
211
 
212
+ // Add timeout for missing organisation context (3 seconds)
213
+ useEffect(() => {
214
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
215
+ const timeoutId = setTimeout(() => {
216
+ setError(new Error('Organisation context is required for permission checks'));
217
+ setIsLoading(false);
218
+ setCan(false);
219
+ }, 3000); // 3 seconds - typical permission check is < 1 second
220
+
221
+ return () => clearTimeout(timeoutId);
222
+ }
223
+ // Clear error if organisation context becomes available
224
+ if (error?.message === 'Organisation context is required for permission checks') {
225
+ setError(null);
226
+ }
227
+ }, [scope.organisationId, error]);
228
+
196
229
  // Use refs to track the last values to prevent unnecessary re-runs
197
230
  const lastUserIdRef = useRef<UUID | null>(null);
198
231
  const lastScopeRef = useRef<string | null>(null);
@@ -226,12 +259,13 @@ export function useCan(
226
259
  return;
227
260
  }
228
261
 
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);
262
+ // Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
263
+ // Wait for organisation context to resolve
264
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
234
265
  setIsLoading(true);
266
+ setCan(false);
267
+ setError(null);
268
+ // Timeout is handled in separate useEffect (Phase 1.4)
235
269
  return;
236
270
  }
237
271
 
@@ -239,21 +273,10 @@ export function useCan(
239
273
  setIsLoading(true);
240
274
  setError(null);
241
275
 
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
276
  const result = useCache
252
277
  ? await isPermittedCached({ userId, scope, permission, pageId })
253
278
  : await isPermitted({ userId, scope, permission, pageId });
254
279
 
255
- console.log('[useCan] Permission check result:', { permission, result, pageId });
256
-
257
280
  setCan(result);
258
281
  } catch (err) {
259
282
  console.error('[useCan] Permission check error:', { permission, error: err });
@@ -275,10 +298,11 @@ export function useCan(
275
298
  return;
276
299
  }
277
300
 
278
- // Don't check permissions if scope is invalid (e.g., organisationId is empty)
279
- if (!scope.organisationId || scope.organisationId.trim() === '') {
301
+ // Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
302
+ if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
280
303
  setCan(false);
281
304
  setIsLoading(true);
305
+ setError(null);
282
306
  return;
283
307
  }
284
308
 
@@ -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 = {
@@ -24,7 +24,9 @@ export class RBACSecurityValidator {
24
24
  }
25
25
 
26
26
  // Permission format: operation:resource[.subresource]
27
- const permissionRegex = /^(read|create|update|delete|manage):[a-z0-9._-]+$/;
27
+ // Only CRUD operations are allowed (read, create, update, delete)
28
+ // The 'manage' operation has been removed for RBAC compliance
29
+ const permissionRegex = /^(read|create|update|delete):[a-z0-9._-]+$/;
28
30
  return permissionRegex.test(permission);
29
31
  }
30
32
 
@@ -53,9 +55,15 @@ export class RBACSecurityValidator {
53
55
  return false;
54
56
  }
55
57
 
56
- // Organisation ID is required for most operations
57
- if (scope.organisationId && !this.validateUUID(scope.organisationId)) {
58
- return false;
58
+ // Organisation ID validation - reject empty strings
59
+ if (scope.organisationId !== undefined) {
60
+ // Reject empty strings - use undefined/null instead
61
+ if (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '') {
62
+ return false;
63
+ }
64
+ if (scope.organisationId && !this.validateUUID(scope.organisationId)) {
65
+ return false;
66
+ }
59
67
  }
60
68
 
61
69
  // Event ID should be a string if provided
@@ -122,7 +130,8 @@ export class RBACSecurityValidator {
122
130
  }
123
131
 
124
132
  const [operation] = permission.split(':');
125
- const hierarchy = ['read', 'create', 'update', 'delete', 'manage'];
133
+ // Only CRUD operations - 'manage' has been removed
134
+ const hierarchy = ['read', 'create', 'update', 'delete'];
126
135
 
127
136
  const permissionLevel = hierarchy.indexOf(operation);
128
137
  const requiredLevel = hierarchy.indexOf(requiredOperation);
@@ -257,10 +266,13 @@ export const DEFAULT_SECURITY_CONFIG: RBACSecurityConfig = {
257
266
 
258
267
  /**
259
268
  * Security context for RBAC operations
269
+ *
270
+ * OrganisationId is required - it can always be derived from event context in event-based apps.
271
+ * If organisation context is not available, the operation should fail rather than proceed without context.
260
272
  */
261
273
  export interface SecurityContext {
262
274
  userId: UUID;
263
- organisationId?: UUID; // Optional for operations without organisation context
275
+ organisationId: UUID; // Required - can always be derived from event context
264
276
  ipAddress?: string;
265
277
  userAgent?: string;
266
278
  timestamp: Date;
@@ -304,7 +316,10 @@ export class RBACSecurityMiddleware {
304
316
  errors.push('Invalid user ID format');
305
317
  }
306
318
 
307
- if (context.organisationId && !RBACSecurityValidator.validateUUID(context.organisationId)) {
319
+ // OrganisationId is required
320
+ if (!context.organisationId) {
321
+ errors.push('Organisation ID is required');
322
+ } else if (!RBACSecurityValidator.validateUUID(context.organisationId)) {
308
323
  errors.push('Invalid organisation ID format');
309
324
  }
310
325
 
@@ -195,7 +195,7 @@ describe('Type Definitions', () => {
195
195
  'create:user-profiles',
196
196
  'update:events',
197
197
  'delete:organisations',
198
- 'manage:base.events'
198
+ 'update:base.events'
199
199
  ];
200
200
 
201
201
  permissions.forEach(permission => {
@@ -206,7 +206,7 @@ describe('Type Definitions', () => {
206
206
  it('supports complex resource names', () => {
207
207
  const permissions: Permission[] = [
208
208
  'read:user-profiles.details',
209
- 'manage:base.events.schedule',
209
+ 'update:base.events.schedule',
210
210
  'create:app-data.records'
211
211
  ];
212
212