@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
@@ -91,11 +91,20 @@ const mockOrganisationContext = {
91
91
  isOrganisationSecure: vi.fn().mockReturnValue(true)
92
92
  };
93
93
 
94
- vi.mock('../../../providers/OrganisationProvider', () => ({
95
- OrganisationProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
94
+ vi.mock('../../../hooks/useOrganisations', () => ({
96
95
  useOrganisations: () => mockOrganisationContext
97
96
  }));
98
97
 
98
+ // Mock useEvents hook (optional - wrapped in try/catch in component)
99
+ vi.mock('../../../providers/EventsProvider', () => ({
100
+ useEvents: vi.fn(() => ({
101
+ selectedEvent: { event_id: 'event-123' },
102
+ events: [],
103
+ isLoading: false,
104
+ error: null,
105
+ })),
106
+ }));
107
+
99
108
  // Mock the new RBAC system for security testing
100
109
  const mockIsPermitted = vi.fn().mockResolvedValue(true);
101
110
  const mockCheckPermission = vi.fn().mockResolvedValue(true);
@@ -108,6 +117,38 @@ vi.mock('../../../rbac/api', () => ({
108
117
  setupRBAC: vi.fn()
109
118
  }));
110
119
 
120
+ // Mock RBAC hooks
121
+ const mockHasPermissionRBAC = vi.fn().mockResolvedValue(true);
122
+ const mockUseRBAC = vi.fn(() => ({
123
+ hasPermission: mockHasPermissionRBAC,
124
+ isLoading: false,
125
+ error: null,
126
+ hasGlobalPermission: vi.fn().mockResolvedValue(true),
127
+ hasOrganisationPermission: vi.fn().mockResolvedValue(true),
128
+ hasEventPermission: vi.fn().mockResolvedValue(true),
129
+ globalRole: null,
130
+ organisationRoles: [],
131
+ eventRoles: [],
132
+ permissionMap: {},
133
+ }));
134
+
135
+ const mockUseCan = vi.fn(() => ({
136
+ can: true,
137
+ isLoading: false,
138
+ error: null,
139
+ refetch: vi.fn().mockResolvedValue(undefined),
140
+ }));
141
+
142
+ vi.mock('../../../rbac/hooks', () => ({
143
+ useRBAC: () => mockUseRBAC(),
144
+ useCan: (...args: any[]) => mockUseCan(...args),
145
+ useResolvedScope: vi.fn(() => ({
146
+ resolvedScope: { organisationId: 'org-123', eventId: 'event-123', appId: 'app-123' },
147
+ isLoading: false,
148
+ error: null,
149
+ })),
150
+ }));
151
+
111
152
  // Mock child components
112
153
  vi.mock('../../Header', () => ({
113
154
  Header: vi.fn(({ appName, user, onSignOut, onChangePassword, onNavigate, currentPath, logo, userMenu, actions }) => (
@@ -186,6 +227,28 @@ describe('PaceAppLayout Security', () => {
186
227
  mockIsPermitted.mockClear();
187
228
  mockIsPermitted.mockResolvedValue(true);
188
229
 
230
+ // Reset RBAC hook mocks
231
+ mockHasPermissionRBAC.mockClear();
232
+ mockHasPermissionRBAC.mockResolvedValue(true);
233
+ mockUseCan.mockReturnValue({
234
+ can: true,
235
+ isLoading: false,
236
+ error: null,
237
+ refetch: vi.fn().mockResolvedValue(undefined),
238
+ });
239
+ mockUseRBAC.mockReturnValue({
240
+ hasPermission: mockHasPermissionRBAC,
241
+ isLoading: false,
242
+ error: null,
243
+ hasGlobalPermission: vi.fn().mockResolvedValue(true),
244
+ hasOrganisationPermission: vi.fn().mockResolvedValue(true),
245
+ hasEventPermission: vi.fn().mockResolvedValue(true),
246
+ globalRole: null,
247
+ organisationRoles: [],
248
+ eventRoles: [],
249
+ permissionMap: {},
250
+ });
251
+
189
252
  // Reset the mocked function
190
253
  const { isPermitted } = await import('../../../rbac/api');
191
254
  vi.mocked(isPermitted).mockClear();
@@ -221,9 +284,13 @@ describe('PaceAppLayout Security', () => {
221
284
  });
222
285
 
223
286
  it('prevents access when user lacks permission', async () => {
224
- // Mock the RBAC system to deny permission
225
- const { isPermitted } = await import('../../../rbac/api');
226
- vi.mocked(isPermitted).mockResolvedValue(false);
287
+ // Mock useCan to return false (deny access)
288
+ mockUseCan.mockReturnValueOnce({
289
+ can: false,
290
+ isLoading: false,
291
+ error: null,
292
+ refetch: vi.fn().mockResolvedValue(undefined),
293
+ });
227
294
 
228
295
  render(
229
296
  <TestWrapper>
@@ -235,8 +302,8 @@ describe('PaceAppLayout Security', () => {
235
302
  await waitFor(() => {
236
303
  expect(screen.getByText('Access Denied')).toBeInTheDocument();
237
304
  expect(screen.getByText("You don't have permission to access this page.")).toBeInTheDocument();
238
- });
239
- });
305
+ }, { timeout: 5000 });
306
+ }, { timeout: 6000 });
240
307
 
241
308
  it('enforces route-specific permissions', async () => {
242
309
  const routePermissions: Record<string, Operation> = {
@@ -263,9 +330,13 @@ describe('PaceAppLayout Security', () => {
263
330
  });
264
331
 
265
332
  it('handles permission check failures securely', async () => {
266
- // Mock the RBAC system to throw an error
267
- const { isPermitted } = await import('../../../rbac/api');
268
- vi.mocked(isPermitted).mockRejectedValue(new Error('Permission check failed'));
333
+ // Mock useCan to return an error state
334
+ mockUseCan.mockReturnValueOnce({
335
+ can: false,
336
+ isLoading: false,
337
+ error: new Error('Permission check failed'),
338
+ refetch: vi.fn().mockResolvedValue(undefined),
339
+ });
269
340
 
270
341
  render(
271
342
  <TestWrapper>
@@ -277,8 +348,8 @@ describe('PaceAppLayout Security', () => {
277
348
  // When permission check throws an error, should show Permission Error page
278
349
  expect(screen.getByText('Permission Error')).toBeInTheDocument();
279
350
  expect(screen.getByText('Permission check failed')).toBeInTheDocument();
280
- });
281
- });
351
+ }, { timeout: 5000 });
352
+ }, { timeout: 6000 });
282
353
 
283
354
  it('prevents bypassing permission checks', async () => {
284
355
  // Test that permission checks cannot be bypassed by manipulating props
@@ -309,12 +380,8 @@ describe('PaceAppLayout Security', () => {
309
380
 
310
381
  describe('Navigation Security', () => {
311
382
  it('filters navigation items based on permissions', async () => {
312
- // Mock permission check to deny access to admin
313
- mockIsPermitted.mockImplementation((userId, scope, operation, pageId) => {
314
- if (pageId === 'admin') return Promise.resolve(false);
315
- return Promise.resolve(true);
316
- });
317
-
383
+ // Mock permission check to allow all permissions for navigation filtering
384
+ // The navigation filtering uses getPermissionMap which is already mocked
318
385
  render(
319
386
  <TestWrapper>
320
387
  <PaceAppLayout
@@ -333,8 +400,8 @@ describe('PaceAppLayout Security', () => {
333
400
  // With permission enforcement enabled, the component should render normally
334
401
  expect(screen.getByTestId('mock-header')).toBeInTheDocument();
335
402
  expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
336
- });
337
- });
403
+ }, { timeout: 5000 });
404
+ }, { timeout: 6000 });
338
405
 
339
406
  it('prevents navigation to unauthorized routes', () => {
340
407
  render(
@@ -596,9 +663,13 @@ describe('PaceAppLayout Security', () => {
596
663
  });
597
664
 
598
665
  it('handles permission errors securely', async () => {
599
- // Mock the RBAC system to deny permission (secure behavior)
600
- const { isPermitted } = await import('../../../rbac/api');
601
- vi.mocked(isPermitted).mockResolvedValue(false);
666
+ // Mock useCan to return false (deny access)
667
+ mockUseCan.mockReturnValueOnce({
668
+ can: false,
669
+ isLoading: false,
670
+ error: null,
671
+ refetch: vi.fn().mockResolvedValue(undefined),
672
+ });
602
673
 
603
674
  render(
604
675
  <TestWrapper>
@@ -608,15 +679,18 @@ describe('PaceAppLayout Security', () => {
608
679
 
609
680
  await waitFor(() => {
610
681
  expect(screen.getByText('Access Denied')).toBeInTheDocument();
611
- });
612
- });
682
+ }, { timeout: 5000 });
683
+ }, { timeout: 6000 });
613
684
 
614
685
  it('prevents information leakage in error messages', async () => {
615
- // Mock the RBAC system to throw an error with sensitive information
616
- const { isPermitted } = await import('../../../rbac/api');
686
+ // Mock useCan to return an error with sensitive information
617
687
  const sensitiveError = new Error('Database connection failed: password=secret123');
618
- vi.mocked(isPermitted).mockClear();
619
- vi.mocked(isPermitted).mockRejectedValue(sensitiveError);
688
+ mockUseCan.mockReturnValueOnce({
689
+ can: false,
690
+ isLoading: false,
691
+ error: sensitiveError,
692
+ refetch: vi.fn().mockResolvedValue(undefined),
693
+ });
620
694
 
621
695
  render(
622
696
  <TestWrapper>
@@ -634,8 +708,8 @@ describe('PaceAppLayout Security', () => {
634
708
  expect(screen.getByText('Permission Error')).toBeInTheDocument();
635
709
  // Should not expose sensitive information
636
710
  expect(screen.queryByText('password=secret123')).not.toBeInTheDocument();
637
- });
638
- });
711
+ }, { timeout: 5000 });
712
+ }, { timeout: 6000 });
639
713
  });
640
714
 
641
715
  describe('Session Security', () => {
@@ -712,16 +786,6 @@ describe('PaceAppLayout Security', () => {
712
786
 
713
787
  it('prevents privilege escalation', async () => {
714
788
  // Test that users cannot escalate their privileges
715
- const { isPermitted } = await import('../../../rbac/api');
716
- vi.mocked(isPermitted).mockImplementation(({ userId, scope, permission, pageId }) => {
717
- // Simulate user trying to access admin with read permission
718
- // Permission is now formatted as "operation:page.pageId"
719
- if (pageId === 'admin' && permission === 'read:page.admin') {
720
- return Promise.resolve(false);
721
- }
722
- return Promise.resolve(true);
723
- });
724
-
725
789
  // Create a test wrapper with admin path for privilege escalation test
726
790
  const AdminTestWrapper = ({ children }: { children: React.ReactNode }) => (
727
791
  <BrowserRouter>
@@ -734,6 +798,14 @@ describe('PaceAppLayout Security', () => {
734
798
  // Mock the location to be /admin for this test
735
799
  mockLocation.pathname = '/admin';
736
800
 
801
+ // Mock useCan to return false (deny admin access)
802
+ mockUseCan.mockReturnValueOnce({
803
+ can: false,
804
+ isLoading: false,
805
+ error: null,
806
+ refetch: vi.fn().mockResolvedValue(undefined),
807
+ });
808
+
737
809
  render(
738
810
  <AdminTestWrapper>
739
811
  <PaceAppLayout
@@ -78,11 +78,20 @@ const mockOrganisationContext = {
78
78
  isOrganisationSecure: vi.fn().mockReturnValue(true)
79
79
  };
80
80
 
81
- vi.mock('../../../providers/OrganisationProvider', () => ({
82
- OrganisationProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
81
+ vi.mock('../../../hooks/useOrganisations', () => ({
83
82
  useOrganisations: () => mockOrganisationContext
84
83
  }));
85
84
 
85
+ // Mock useEvents hook (optional - wrapped in try/catch in component)
86
+ vi.mock('../../../providers/EventsProvider', () => ({
87
+ useEvents: vi.fn(() => ({
88
+ selectedEvent: { event_id: 'event-123' },
89
+ events: [],
90
+ isLoading: false,
91
+ error: null,
92
+ })),
93
+ }));
94
+
86
95
  // Mock the new RBAC system
87
96
  const mockIsPermitted = vi.fn().mockImplementation((input) => {
88
97
  console.log('[PaceAppLayout] Page access attempt:', {
@@ -104,6 +113,38 @@ vi.mock('../../../rbac/api', () => ({
104
113
  setupRBAC: vi.fn()
105
114
  }));
106
115
 
116
+ // Mock RBAC hooks
117
+ const mockHasPermissionRBAC = vi.fn().mockResolvedValue(true);
118
+ const mockUseRBAC = vi.fn(() => ({
119
+ hasPermission: mockHasPermissionRBAC,
120
+ isLoading: false,
121
+ error: null,
122
+ hasGlobalPermission: vi.fn().mockResolvedValue(true),
123
+ hasOrganisationPermission: vi.fn().mockResolvedValue(true),
124
+ hasEventPermission: vi.fn().mockResolvedValue(true),
125
+ globalRole: null,
126
+ organisationRoles: [],
127
+ eventRoles: [],
128
+ permissionMap: {},
129
+ }));
130
+
131
+ const mockUseCan = vi.fn(() => ({
132
+ can: true,
133
+ isLoading: false,
134
+ error: null,
135
+ refetch: vi.fn().mockResolvedValue(undefined),
136
+ }));
137
+
138
+ vi.mock('../../../rbac/hooks', () => ({
139
+ useRBAC: () => mockUseRBAC(),
140
+ useCan: (...args: any[]) => mockUseCan(...args),
141
+ useResolvedScope: vi.fn(() => ({
142
+ resolvedScope: { organisationId: 'org-123', eventId: 'event-123', appId: 'app-123' },
143
+ isLoading: false,
144
+ error: null,
145
+ })),
146
+ }));
147
+
107
148
  // Mock Footer (static, doesn't depend on props)
108
149
  vi.mock('../../Footer', () => ({
109
150
  Footer: vi.fn(() => <footer data-testid="mock-footer" role="contentinfo">Mock Footer</footer>)
@@ -194,6 +235,27 @@ describe('PaceAppLayout Component', () => {
194
235
  // Explicitly re-mock updatePassword to always return { error: null }
195
236
  mockUpdatePassword.mockResolvedValue({ error: null });
196
237
 
238
+ // Reset RBAC mocks to default state
239
+ mockHasPermissionRBAC.mockResolvedValue(true);
240
+ mockUseCan.mockReturnValue({
241
+ can: true,
242
+ isLoading: false,
243
+ error: null,
244
+ refetch: vi.fn().mockResolvedValue(undefined),
245
+ });
246
+ mockUseRBAC.mockReturnValue({
247
+ hasPermission: mockHasPermissionRBAC,
248
+ isLoading: false,
249
+ error: null,
250
+ hasGlobalPermission: vi.fn().mockResolvedValue(true),
251
+ hasOrganisationPermission: vi.fn().mockResolvedValue(true),
252
+ hasEventPermission: vi.fn().mockResolvedValue(true),
253
+ globalRole: null,
254
+ organisationRoles: [],
255
+ eventRoles: [],
256
+ permissionMap: {},
257
+ });
258
+
197
259
  // Reset location mock
198
260
  Object.defineProperty(window, 'location', {
199
261
  value: { pathname: '/test-path' },
@@ -519,7 +581,11 @@ describe('PaceAppLayout Component', () => {
519
581
  </TestWrapper>
520
582
  );
521
583
 
522
- expect(mockIsPermitted).not.toHaveBeenCalled();
584
+ // useCan is always called for consistency, but when enforcePermissions is false,
585
+ // the result is ignored (hasPermission = enforcePermissions ? can : true)
586
+ // So the component should render normally regardless of permission check result
587
+ expect(screen.getByTestId('mock-header')).toBeInTheDocument();
588
+ expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
523
589
  });
524
590
 
525
591
  it('enables permission enforcement when enforcePermissions is true', async () => {
@@ -569,13 +635,15 @@ describe('PaceAppLayout Component', () => {
569
635
  );
570
636
 
571
637
  await waitFor(() => {
572
- expect(mockIsPermitted).toHaveBeenCalledWith({
573
- userId: 'test-user-id',
574
- scope: expect.objectContaining({ organisationId: 'test-org-123' }),
575
- permission: 'delete:page.test-path',
576
- pageId: 'test-path'
577
- });
578
- });
638
+ // useCan is called with userId, scope, permission, pageId, useCache
639
+ expect(mockUseCan).toHaveBeenCalledWith(
640
+ 'test-user-id',
641
+ expect.objectContaining({ organisationId: 'org-123' }),
642
+ 'delete:page.test-path',
643
+ 'test-path',
644
+ true
645
+ );
646
+ }, { timeout: 2000 });
579
647
  });
580
648
 
581
649
  it('uses custom page ID mapping when provided', async () => {
@@ -594,18 +662,25 @@ describe('PaceAppLayout Component', () => {
594
662
  );
595
663
 
596
664
  await waitFor(() => {
597
- expect(mockIsPermitted).toHaveBeenCalledWith({
598
- userId: 'test-user-id',
599
- scope: expect.objectContaining({ organisationId: 'test-org-123' }),
600
- permission: 'read:page.custom-page-id',
601
- pageId: 'custom-page-id'
602
- });
603
- });
665
+ // useCan is called with userId, scope, permission, pageId, useCache
666
+ expect(mockUseCan).toHaveBeenCalledWith(
667
+ 'test-user-id',
668
+ expect.objectContaining({ organisationId: 'org-123' }),
669
+ 'read:page.custom-page-id',
670
+ 'custom-page-id',
671
+ true
672
+ );
673
+ }, { timeout: 2000 });
604
674
  });
605
675
 
606
676
  it('shows loading state while checking permissions', async () => {
607
- // Make permission check take time
608
- mockIsPermitted.mockImplementation((input) => new Promise(resolve => setTimeout(() => resolve(true), 100)));
677
+ // Mock useCan to return loading state
678
+ mockUseCan.mockReturnValueOnce({
679
+ can: false,
680
+ isLoading: true,
681
+ error: null,
682
+ refetch: vi.fn().mockResolvedValue(undefined),
683
+ });
609
684
 
610
685
  render(
611
686
  <TestWrapper>
@@ -618,7 +693,14 @@ describe('PaceAppLayout Component', () => {
618
693
 
619
694
  it('shows permission error when checkPermission throws', async () => {
620
695
  const mockError = new Error('Permission check failed');
621
- mockIsPermitted.mockRejectedValue(mockError);
696
+
697
+ // Mock useCan to return an error state
698
+ mockUseCan.mockReturnValueOnce({
699
+ can: false,
700
+ isLoading: false,
701
+ error: mockError,
702
+ refetch: vi.fn().mockResolvedValue(undefined),
703
+ });
622
704
 
623
705
  render(
624
706
  <TestWrapper>
@@ -629,11 +711,17 @@ describe('PaceAppLayout Component', () => {
629
711
  await waitFor(() => {
630
712
  expect(screen.getByText('Permission Error')).toBeInTheDocument();
631
713
  expect(screen.getByText('Permission check failed')).toBeInTheDocument();
632
- }, { timeout: 2000 });
633
- });
714
+ }, { timeout: 5000 });
715
+ }, { timeout: 6000 });
634
716
 
635
717
  it('shows access denied when user lacks permission', async () => {
636
- mockIsPermitted.mockResolvedValue(false);
718
+ // Mock useCan to return false (user lacks permission)
719
+ mockUseCan.mockReturnValueOnce({
720
+ can: false,
721
+ isLoading: false,
722
+ error: null,
723
+ refetch: vi.fn().mockResolvedValue(undefined),
724
+ });
637
725
 
638
726
  render(
639
727
  <TestWrapper>
@@ -644,11 +732,18 @@ describe('PaceAppLayout Component', () => {
644
732
  await waitFor(() => {
645
733
  expect(screen.getByText('Access Denied')).toBeInTheDocument();
646
734
  expect(screen.getByText("You don't have permission to access this page.")).toBeInTheDocument();
647
- });
648
- });
735
+ }, { timeout: 5000 });
736
+ }, { timeout: 6000 });
649
737
 
650
738
  it('shows custom permission fallback when provided', async () => {
651
- mockIsPermitted.mockResolvedValue(false);
739
+ // Mock useCan to return false
740
+ mockUseCan.mockReturnValueOnce({
741
+ can: false,
742
+ isLoading: false,
743
+ error: null,
744
+ refetch: vi.fn().mockResolvedValue(undefined),
745
+ });
746
+
652
747
  const CustomFallback = () => <div data-testid="custom-fallback">Custom Access Denied</div>;
653
748
 
654
749
  render(
@@ -663,11 +758,17 @@ describe('PaceAppLayout Component', () => {
663
758
 
664
759
  await waitFor(() => {
665
760
  expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
666
- });
667
- });
761
+ }, { timeout: 5000 });
762
+ }, { timeout: 6000 });
668
763
 
669
764
  it('provides go home button in access denied state', async () => {
670
- mockIsPermitted.mockResolvedValue(false);
765
+ // Mock useCan to return false
766
+ mockUseCan.mockReturnValueOnce({
767
+ can: false,
768
+ isLoading: false,
769
+ error: null,
770
+ refetch: vi.fn().mockResolvedValue(undefined),
771
+ });
671
772
 
672
773
  render(
673
774
  <TestWrapper>
@@ -681,12 +782,19 @@ describe('PaceAppLayout Component', () => {
681
782
 
682
783
  fireEvent.click(goHomeButton);
683
784
  expect(mockNavigate).toHaveBeenCalledWith('/');
684
- }, { timeout: 2000 });
685
- });
785
+ }, { timeout: 5000 });
786
+ }, { timeout: 6000 });
686
787
 
687
788
  it('provides go home button in permission error state', async () => {
688
789
  const mockError = new Error('Permission check failed');
689
- mockIsPermitted.mockRejectedValue(mockError);
790
+
791
+ // Mock useCan to return an error state
792
+ mockUseCan.mockReturnValueOnce({
793
+ can: false,
794
+ isLoading: false,
795
+ error: mockError,
796
+ refetch: vi.fn().mockResolvedValue(undefined),
797
+ });
690
798
 
691
799
  render(
692
800
  <TestWrapper>
@@ -700,8 +808,8 @@ describe('PaceAppLayout Component', () => {
700
808
 
701
809
  fireEvent.click(goHomeButton);
702
810
  expect(mockNavigate).toHaveBeenCalledWith('/');
703
- });
704
- });
811
+ }, { timeout: 5000 });
812
+ }, { timeout: 6000 });
705
813
  });
706
814
 
707
815
  describe('Navigation Filtering by Permissions', () => {
@@ -736,9 +844,9 @@ describe('PaceAppLayout Component', () => {
736
844
  // Wait a bit to see if the component renders
737
845
  await new Promise(resolve => setTimeout(resolve, 100));
738
846
 
739
- // Note: The filtering is async, so we need to wait with a timeout
847
+ // Note: The filtering uses getPermissionMap which is mocked, so just verify rendering
740
848
  await waitFor(() => {
741
- expect(mockIsPermitted).toHaveBeenCalled();
849
+ expect(screen.getByTestId('mock-header')).toBeInTheDocument();
742
850
  }, { timeout: 2000 });
743
851
 
744
852
  // Verify the component rendered successfully
@@ -826,13 +934,15 @@ describe('PaceAppLayout Component', () => {
826
934
  );
827
935
 
828
936
  await waitFor(() => {
829
- expect(mockIsPermitted).toHaveBeenCalledWith({
830
- userId: 'test-user-id',
831
- scope: expect.objectContaining({ organisationId: 'test-org-123' }),
832
- permission: 'read:page.new-path',
833
- pageId: 'new-path'
834
- });
835
- });
937
+ // useCan is called with userId, scope, permission, pageId, useCache
938
+ expect(mockUseCan).toHaveBeenCalledWith(
939
+ 'test-user-id',
940
+ expect.objectContaining({ organisationId: 'org-123' }),
941
+ 'read:page.new-path',
942
+ 'new-path',
943
+ true
944
+ );
945
+ }, { timeout: 2000 });
836
946
 
837
947
  // Reset for other tests
838
948
  mockLocation.pathname = '/test-path';
@@ -894,8 +1004,8 @@ describe('PaceAppLayout Component', () => {
894
1004
  );
895
1005
 
896
1006
  await waitFor(() => {
897
- expect(mockIsPermitted).toHaveBeenCalledTimes(1);
898
- });
1007
+ expect(mockUseCan).toHaveBeenCalled();
1008
+ }, { timeout: 2000 });
899
1009
  });
900
1010
  });
901
1011
  });
@@ -121,13 +121,14 @@
121
121
  * - Tailwind CSS - Styling
122
122
  */
123
123
 
124
- import React, { useEffect, useState } from 'react';
124
+ import React, { useEffect, useState, useContext } from 'react';
125
125
  import { useNavigate } from 'react-router-dom';
126
126
  import { useUnifiedAuth } from '../../providers';
127
127
  import { isSuperAdmin } from '../../rbac/api';
128
128
  import { LoginForm } from '../LoginForm';
129
129
  import { Button, Input, Label } from '..';
130
130
  import { clearPalette } from '../../theming/runtime';
131
+ import { EventServiceContext } from '../../providers/services/EventServiceProvider';
131
132
 
132
133
  export interface PaceLoginPageProps {
133
134
  /** The name of the application to be displayed on the login form. */
@@ -174,12 +175,40 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
174
175
  const [accessError, setAccessError] = useState<string | null>(null);
175
176
  const [isCheckingAccess, setIsCheckingAccess] = useState(false);
176
177
 
178
+ // Get event service context (may not be available if outside EventServiceProvider)
179
+ // Using useContext directly allows graceful handling when provider is not available
180
+ const eventServiceContext = useContext(EventServiceContext);
181
+ const eventService = eventServiceContext?.eventService || null;
182
+
177
183
  // Clear any active event theme when login page mounts
178
184
  // This ensures the login screen always uses default colors
179
185
  useEffect(() => {
180
186
  clearPalette();
181
187
  }, []);
182
188
 
189
+ // Restore persisted event after login screen has rendered
190
+ // This happens after the login page is fully rendered, allowing events to be loaded first
191
+ useEffect(() => {
192
+ const restoreEvent = async () => {
193
+ try {
194
+ const isOnLoginPage = window.location.pathname === '/login' || window.location.pathname.startsWith('/login');
195
+ if (isOnLoginPage && eventService) {
196
+ await eventService.restorePersistedEvent();
197
+ }
198
+ } catch (error) {
199
+ // Service may not be available yet or events not loaded - that's okay
200
+ console.debug('[PaceLoginPage] Could not restore persisted event (service may not be ready):', error);
201
+ }
202
+ };
203
+
204
+ // Small delay to ensure login page is fully rendered before restoring
205
+ const timeoutId = setTimeout(() => {
206
+ restoreEvent();
207
+ }, 100);
208
+
209
+ return () => clearTimeout(timeoutId);
210
+ }, [eventService]);
211
+
183
212
  // Check app access after authentication using RBAC
184
213
  useEffect(() => {
185
214
  if (!requireAppAccess || !isAuthenticated || isLoading || !user || !supabase) {