@jmruthers/pace-core 0.5.109 → 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.
- package/CHANGELOG.md +22 -0
- package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-5HITILXS.js → DataTable-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-5I3E47G2.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-P72NKAT5.js → chunk-2BIDKXQU.js} +157 -120
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-S4D3Z723.js → chunk-ACYQNYHB.js} +7 -7
- package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
- package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
- package/dist/chunk-IWJYNWXN.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
- package/dist/chunk-MW73E7SP.js.map +1 -0
- package/dist/{chunk-F6TSYCKP.js → chunk-PXXS26G5.js} +68 -29
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-UW2DE6JX.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-WWNOVFDC.js → chunk-UGVU7L7N.js} +52 -90
- package/dist/chunk-UGVU7L7N.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
- package/dist/chunk-X7SPKHYZ.js.map +1 -0
- package/dist/{chunk-3TKTL5AZ.js → chunk-ZL45MG76.js} +60 -60
- package/dist/chunk-ZL45MG76.js.map +1 -0
- package/dist/components.js +10 -10
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +9 -7
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +46 -29
- package/dist/rbac/index.js +9 -9
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +8 -8
- package/docs/api/classes/RBACEngine.md +9 -8
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +19 -8
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +19 -6
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +44 -43
- package/docs/api-reference/hooks.md +8 -4
- package/docs/architecture/rpc-function-standards.md +3 -1
- package/docs/best-practices/common-patterns.md +3 -3
- package/docs/best-practices/deployment.md +10 -4
- package/docs/best-practices/performance.md +11 -3
- package/docs/core-concepts/organisations.md +8 -8
- package/docs/core-concepts/permissions.md +133 -72
- package/docs/documentation-index.md +0 -2
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/README.md +114 -38
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/api-reference.md +63 -16
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +19 -19
- package/docs/rbac/quick-start.md +110 -35
- package/docs/rbac/troubleshooting.md +127 -3
- package/package.json +1 -1
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
- package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
- package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
- package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useFileDisplay.ts +51 -0
- package/src/hooks/usePermissionCache.test.ts +112 -68
- package/src/hooks/usePermissionCache.ts +55 -15
- package/src/rbac/README.md +81 -39
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
- package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
- package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
- package/src/rbac/adapters.tsx +4 -4
- package/src/rbac/api.test.ts +39 -15
- package/src/rbac/api.ts +27 -9
- package/src/rbac/audit.test.ts +2 -2
- package/src/rbac/audit.ts +14 -5
- package/src/rbac/cache.test.ts +12 -0
- package/src/rbac/cache.ts +29 -9
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +14 -14
- package/src/rbac/components/NavigationProvider.test.tsx +1 -1
- package/src/rbac/components/PagePermissionGuard.tsx +22 -38
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +19 -15
- package/src/rbac/components/RoleBasedRouter.tsx +16 -9
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +2 -2
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/config.ts +2 -0
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +27 -7
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +47 -23
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
- package/src/rbac/hooks/useRBAC.test.ts +3 -40
- package/src/rbac/hooks/useRBAC.ts +0 -55
- package/src/rbac/hooks/useResolvedScope.ts +23 -31
- package/src/rbac/permissions.test.ts +11 -7
- package/src/rbac/security.test.ts +2 -2
- package/src/rbac/security.ts +23 -8
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +41 -13
- package/src/services/__tests__/EventService.test.ts +25 -4
- package/src/services/interfaces/IEventService.ts +1 -0
- package/src/utils/file-reference.ts +9 -0
- package/dist/chunk-2W4WKJVF.js.map +0 -1
- package/dist/chunk-3TKTL5AZ.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-F6TSYCKP.js.map +0 -1
- package/dist/chunk-P72NKAT5.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-WWNOVFDC.js.map +0 -1
- package/docs/rbac/breaking-changes-v3.md +0 -222
- package/docs/rbac/migration-guide.md +0 -260
- /package/dist/{DataTable-5HITILXS.js.map → DataTable-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-5I3E47G2.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-S4D3Z723.js.map → chunk-ACYQNYHB.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
- /package/dist/{chunk-UW2DE6JX.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /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('../../../
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
313
|
-
|
|
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
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
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
|
-
|
|
619
|
-
|
|
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('../../../
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
//
|
|
608
|
-
|
|
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
|
-
|
|
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:
|
|
633
|
-
});
|
|
714
|
+
}, { timeout: 5000 });
|
|
715
|
+
}, { timeout: 6000 });
|
|
634
716
|
|
|
635
717
|
it('shows access denied when user lacks permission', async () => {
|
|
636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
847
|
+
// Note: The filtering uses getPermissionMap which is mocked, so just verify rendering
|
|
740
848
|
await waitFor(() => {
|
|
741
|
-
expect(
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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(
|
|
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) {
|