@jmruthers/pace-core 0.5.105 → 0.5.107
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/dist/{DataTable-BE0OXZKQ.d.ts → DataTable-D5cBRca8.d.ts} +1 -1
- package/dist/{DataTable-LWHFLTEW.js → DataTable-H2WIR2DN.js} +3 -3
- package/dist/{chunk-QPCAGLUS.js → chunk-4OX5PXHX.js} +5 -2
- package/dist/chunk-4OX5PXHX.js.map +1 -0
- package/dist/{chunk-75G3NZWN.js → chunk-5JJCXTVE.js} +293 -37
- package/dist/chunk-5JJCXTVE.js.map +1 -0
- package/dist/{chunk-HBGPLSA5.js → chunk-DMNMZKWS.js} +70 -24
- package/dist/chunk-DMNMZKWS.js.map +1 -0
- package/dist/{chunk-AZFPGDCJ.js → chunk-EWKCROSF.js} +133 -49
- package/dist/chunk-EWKCROSF.js.map +1 -0
- package/dist/{chunk-4BWGRQBG.js → chunk-NFPV7MRN.js} +22 -2
- package/dist/chunk-NFPV7MRN.js.map +1 -0
- package/dist/{chunk-DWYMGSGU.js → chunk-VJ7MPS2K.js} +2 -2
- package/dist/components.d.ts +3 -3
- package/dist/components.js +4 -4
- package/dist/{formatting-BfDeV-ja.d.ts → formatting-BiEv5oEk.d.ts} +32 -2
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +3 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +6 -6
- package/dist/{types-BDg1mAGG.d.ts → types-D4TVpDa1.d.ts} +24 -1
- package/dist/{useToast-Bm6TnSK-.d.ts → useToast-DRah6K-g.d.ts} +5 -2
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- 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 +4 -4
- 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 +18 -18
- package/docs/api/interfaces/DataTableColumn.md +115 -10
- package/docs/api/interfaces/DataTableProps.md +38 -38
- package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
- package/docs/api/interfaces/EmptyStateConfig.md +5 -5
- 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 +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- 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 +39 -18
- package/docs/api-reference/utilities.md +26 -3
- package/docs/implementation-guides/data-tables.md +390 -0
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.tsx +4 -0
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +25 -10
- package/src/components/DataTable/components/EditableRow.tsx +174 -16
- package/src/components/DataTable/components/UnifiedTableBody.tsx +205 -35
- package/src/components/DataTable/types.ts +34 -4
- package/src/components/FileDisplay/FileDisplay.test.tsx +184 -201
- package/src/components/FileDisplay/FileDisplay.tsx +40 -39
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +189 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +142 -35
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +4 -4
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/public/usePublicFileDisplay.ts +25 -15
- package/src/hooks/useEventTheme.test.ts +11 -0
- package/src/hooks/useFileDisplay.ts +11 -0
- package/src/hooks/useSecureDataAccess.test.ts +22 -5
- package/src/hooks/useToast.ts +11 -2
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +67 -3
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +72 -4
- package/src/services/__tests__/OrganisationService.pagination.test.ts +10 -2
- package/src/styles/core.css +11 -0
- package/src/utils/__tests__/formatting.unit.test.ts +33 -0
- package/src/utils/file-reference.test.ts +44 -5
- package/src/utils/file-reference.ts +49 -26
- package/src/utils/formatting.ts +57 -2
- package/src/validation/__tests__/passwordSchema.unit.test.ts +3 -3
- package/dist/chunk-4BWGRQBG.js.map +0 -1
- package/dist/chunk-75G3NZWN.js.map +0 -1
- package/dist/chunk-AZFPGDCJ.js.map +0 -1
- package/dist/chunk-HBGPLSA5.js.map +0 -1
- package/dist/chunk-QPCAGLUS.js.map +0 -1
- /package/dist/{DataTable-LWHFLTEW.js.map → DataTable-H2WIR2DN.js.map} +0 -0
- /package/dist/{chunk-DWYMGSGU.js.map → chunk-VJ7MPS2K.js.map} +0 -0
|
@@ -21,9 +21,11 @@ const mockAuthContext = {
|
|
|
21
21
|
refreshSession: vi.fn(),
|
|
22
22
|
appName: 'Test App',
|
|
23
23
|
hasErrors: false,
|
|
24
|
-
selectedOrganisation: null,
|
|
24
|
+
selectedOrganisation: { id: 'test-org-1', name: 'Test Org', display_name: 'Test Organisation', subscription_tier: 'basic', settings: {}, is_active: true, parent_id: null, created_at: '', updated_at: '' },
|
|
25
25
|
organisations: [],
|
|
26
|
+
selectedEvent: null,
|
|
26
27
|
events: [],
|
|
28
|
+
supabase: {},
|
|
27
29
|
// Note: hasPermission, hasRole, hasAccessLevel, permissions, roles, and accessLevel
|
|
28
30
|
// were removed from UnifiedAuthProvider. Use useRBAC() hook for permissions instead.
|
|
29
31
|
// Inactivity context
|
|
@@ -39,6 +41,50 @@ vi.mock('../../providers/UnifiedAuthProvider', () => ({
|
|
|
39
41
|
useUnifiedAuth: vi.fn(() => mockAuthContext),
|
|
40
42
|
}));
|
|
41
43
|
|
|
44
|
+
// Mock RBAC hooks with hoisted mocks so they can be controlled per test
|
|
45
|
+
const mockUseRBAC = vi.hoisted(() => vi.fn(() => ({
|
|
46
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
47
|
+
globalRole: null,
|
|
48
|
+
organisationRole: null,
|
|
49
|
+
eventAppRole: null,
|
|
50
|
+
hasPermission: vi.fn(),
|
|
51
|
+
hasGlobalPermission: vi.fn(),
|
|
52
|
+
isSuperAdmin: false,
|
|
53
|
+
isOrgAdmin: false,
|
|
54
|
+
isEventAdmin: false,
|
|
55
|
+
canManageOrganisation: false,
|
|
56
|
+
canManageEvent: false,
|
|
57
|
+
isLoading: false,
|
|
58
|
+
error: null,
|
|
59
|
+
})));
|
|
60
|
+
|
|
61
|
+
const mockUseResolvedScope = vi.hoisted(() => vi.fn(() => ({
|
|
62
|
+
resolvedScope: { organisationId: 'test-org-1', eventId: undefined, appId: undefined },
|
|
63
|
+
isLoading: false,
|
|
64
|
+
})));
|
|
65
|
+
|
|
66
|
+
const mockUsePermissions = vi.hoisted(() => vi.fn(() => ({
|
|
67
|
+
permissions: {} as any,
|
|
68
|
+
isLoading: false,
|
|
69
|
+
error: null,
|
|
70
|
+
hasPermission: vi.fn(() => false),
|
|
71
|
+
hasAnyPermission: vi.fn(() => false),
|
|
72
|
+
hasAllPermissions: vi.fn(() => false),
|
|
73
|
+
refetch: vi.fn(),
|
|
74
|
+
})));
|
|
75
|
+
|
|
76
|
+
vi.mock('../../rbac/hooks/useRBAC', () => ({
|
|
77
|
+
useRBAC: mockUseRBAC,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
vi.mock('../../rbac/hooks', () => ({
|
|
81
|
+
useResolvedScope: mockUseResolvedScope,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
vi.mock('../../rbac/hooks/usePermissions', () => ({
|
|
85
|
+
usePermissions: mockUsePermissions,
|
|
86
|
+
}));
|
|
87
|
+
|
|
42
88
|
// Mock console methods to avoid noise in tests
|
|
43
89
|
const originalConsoleLog = console.log;
|
|
44
90
|
const originalConsoleWarn = console.warn;
|
|
@@ -381,15 +427,41 @@ describe('NavigationMenu Component', () => {
|
|
|
381
427
|
// Permission-based filtering tests
|
|
382
428
|
describe('Permission-Based Filtering', () => {
|
|
383
429
|
beforeEach(() => {
|
|
384
|
-
// Note: Permission checks are currently disabled in NavigationMenu
|
|
385
|
-
// until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
|
|
386
430
|
vi.clearAllMocks();
|
|
387
431
|
});
|
|
388
432
|
|
|
389
|
-
it
|
|
390
|
-
// TODO: This test is skipped because NavigationMenu permission filtering is temporarily disabled
|
|
391
|
-
// until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
|
|
433
|
+
it('renders items with permission requirements', async () => {
|
|
392
434
|
const user = userEvent.setup();
|
|
435
|
+
|
|
436
|
+
// Mock RBAC hooks to grant permissions and roles
|
|
437
|
+
mockUseRBAC.mockReturnValue({
|
|
438
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
439
|
+
globalRole: null,
|
|
440
|
+
organisationRole: 'org_admin' as any,
|
|
441
|
+
eventAppRole: 'event_admin' as any,
|
|
442
|
+
hasPermission: vi.fn(),
|
|
443
|
+
hasGlobalPermission: vi.fn(),
|
|
444
|
+
isSuperAdmin: false,
|
|
445
|
+
isOrgAdmin: true,
|
|
446
|
+
isEventAdmin: true,
|
|
447
|
+
canManageOrganisation: true,
|
|
448
|
+
canManageEvent: true,
|
|
449
|
+
isLoading: false,
|
|
450
|
+
error: null,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
mockUsePermissions.mockReturnValue({
|
|
454
|
+
permissions: {
|
|
455
|
+
'dashboard:read': true,
|
|
456
|
+
} as any,
|
|
457
|
+
isLoading: false,
|
|
458
|
+
error: null,
|
|
459
|
+
hasPermission: vi.fn((p: any) => p === 'dashboard:read'),
|
|
460
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => p === 'dashboard:read')),
|
|
461
|
+
hasAllPermissions: vi.fn(() => true),
|
|
462
|
+
refetch: vi.fn(),
|
|
463
|
+
});
|
|
464
|
+
|
|
393
465
|
renderWithProviders(
|
|
394
466
|
<NavigationMenu
|
|
395
467
|
items={permissionBasedNavItems}
|
|
@@ -507,11 +579,38 @@ describe('NavigationMenu Component', () => {
|
|
|
507
579
|
expect(window.location.href).toBe('/');
|
|
508
580
|
});
|
|
509
581
|
|
|
510
|
-
it
|
|
511
|
-
// TODO: This test is skipped because NavigationMenu permission filtering is temporarily disabled
|
|
512
|
-
// until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
|
|
582
|
+
it('handles permission-based navigation', async () => {
|
|
513
583
|
const user = userEvent.setup();
|
|
514
584
|
|
|
585
|
+
// Mock RBAC hooks to grant permissions and roles
|
|
586
|
+
mockUseRBAC.mockReturnValue({
|
|
587
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
588
|
+
globalRole: null,
|
|
589
|
+
organisationRole: 'org_admin' as any,
|
|
590
|
+
eventAppRole: 'event_admin' as any,
|
|
591
|
+
hasPermission: vi.fn(),
|
|
592
|
+
hasGlobalPermission: vi.fn(),
|
|
593
|
+
isSuperAdmin: false,
|
|
594
|
+
isOrgAdmin: true,
|
|
595
|
+
isEventAdmin: true,
|
|
596
|
+
canManageOrganisation: true,
|
|
597
|
+
canManageEvent: true,
|
|
598
|
+
isLoading: false,
|
|
599
|
+
error: null,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
mockUsePermissions.mockReturnValue({
|
|
603
|
+
permissions: {
|
|
604
|
+
'dashboard:read': true,
|
|
605
|
+
} as any,
|
|
606
|
+
isLoading: false,
|
|
607
|
+
error: null,
|
|
608
|
+
hasPermission: vi.fn((p: any) => p === 'dashboard:read'),
|
|
609
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => p === 'dashboard:read')),
|
|
610
|
+
hasAllPermissions: vi.fn(() => true),
|
|
611
|
+
refetch: vi.fn(),
|
|
612
|
+
});
|
|
613
|
+
|
|
515
614
|
renderWithProviders(
|
|
516
615
|
<NavigationMenu
|
|
517
616
|
items={permissionBasedNavItems}
|
|
@@ -530,12 +629,52 @@ describe('NavigationMenu Component', () => {
|
|
|
530
629
|
expect(screen.getByText('Users')).toBeInTheDocument();
|
|
531
630
|
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
532
631
|
}, { interval: 10 });
|
|
632
|
+
|
|
633
|
+
// Click on an item to verify navigation works
|
|
634
|
+
const dashboardItem = screen.getByText('Dashboard');
|
|
635
|
+
await user.click(dashboardItem);
|
|
636
|
+
|
|
637
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
638
|
+
expect.objectContaining({
|
|
639
|
+
id: 'dashboard',
|
|
640
|
+
label: 'Dashboard',
|
|
641
|
+
href: '/dashboard',
|
|
642
|
+
})
|
|
643
|
+
);
|
|
533
644
|
});
|
|
534
645
|
|
|
535
|
-
it
|
|
536
|
-
// TODO: This test is skipped because NavigationMenu permission filtering is temporarily disabled
|
|
537
|
-
// until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
|
|
646
|
+
it('handles strict mode configuration', async () => {
|
|
538
647
|
const user = userEvent.setup();
|
|
648
|
+
|
|
649
|
+
// Mock RBAC hooks to grant permissions and roles
|
|
650
|
+
mockUseRBAC.mockReturnValue({
|
|
651
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
652
|
+
globalRole: null,
|
|
653
|
+
organisationRole: 'org_admin' as any,
|
|
654
|
+
eventAppRole: 'event_admin' as any,
|
|
655
|
+
hasPermission: vi.fn(),
|
|
656
|
+
hasGlobalPermission: vi.fn(),
|
|
657
|
+
isSuperAdmin: false,
|
|
658
|
+
isOrgAdmin: true,
|
|
659
|
+
isEventAdmin: true,
|
|
660
|
+
canManageOrganisation: true,
|
|
661
|
+
canManageEvent: true,
|
|
662
|
+
isLoading: false,
|
|
663
|
+
error: null,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
mockUsePermissions.mockReturnValue({
|
|
667
|
+
permissions: {
|
|
668
|
+
'dashboard:read': true,
|
|
669
|
+
} as any,
|
|
670
|
+
isLoading: false,
|
|
671
|
+
error: null,
|
|
672
|
+
hasPermission: vi.fn((p: any) => p === 'dashboard:read'),
|
|
673
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => p === 'dashboard:read')),
|
|
674
|
+
hasAllPermissions: vi.fn(() => true),
|
|
675
|
+
refetch: vi.fn(),
|
|
676
|
+
});
|
|
677
|
+
|
|
539
678
|
renderWithProviders(
|
|
540
679
|
<NavigationMenu
|
|
541
680
|
items={permissionBasedNavItems}
|
|
@@ -555,6 +694,14 @@ describe('NavigationMenu Component', () => {
|
|
|
555
694
|
expect(screen.getByText('Users')).toBeInTheDocument();
|
|
556
695
|
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
557
696
|
}, { interval: 10 });
|
|
697
|
+
|
|
698
|
+
// Verify strict mode is working - navigation should proceed with permissions
|
|
699
|
+
const dashboardItem = screen.getByText('Dashboard');
|
|
700
|
+
await user.click(dashboardItem);
|
|
701
|
+
|
|
702
|
+
expect(mockNavigate).toHaveBeenCalled();
|
|
703
|
+
// Should not trigger strict mode violation since user has permissions
|
|
704
|
+
expect(mockOnStrictModeViolation).not.toHaveBeenCalled();
|
|
558
705
|
});
|
|
559
706
|
});
|
|
560
707
|
|
|
@@ -693,6 +840,35 @@ describe('NavigationMenu Component', () => {
|
|
|
693
840
|
},
|
|
694
841
|
];
|
|
695
842
|
|
|
843
|
+
// Mock RBAC to grant the valid permission
|
|
844
|
+
mockUseRBAC.mockReturnValue({
|
|
845
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
846
|
+
globalRole: null,
|
|
847
|
+
organisationRole: null,
|
|
848
|
+
eventAppRole: null,
|
|
849
|
+
hasPermission: vi.fn(),
|
|
850
|
+
hasGlobalPermission: vi.fn(),
|
|
851
|
+
isSuperAdmin: false,
|
|
852
|
+
isOrgAdmin: false,
|
|
853
|
+
isEventAdmin: false,
|
|
854
|
+
canManageOrganisation: false,
|
|
855
|
+
canManageEvent: false,
|
|
856
|
+
isLoading: false,
|
|
857
|
+
error: null,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
mockUsePermissions.mockReturnValue({
|
|
861
|
+
permissions: {
|
|
862
|
+
'valid-permission': true,
|
|
863
|
+
} as any,
|
|
864
|
+
isLoading: false,
|
|
865
|
+
error: null,
|
|
866
|
+
hasPermission: vi.fn((p: any) => p === 'valid-permission'),
|
|
867
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => typeof p === 'string' && p === 'valid-permission')),
|
|
868
|
+
hasAllPermissions: vi.fn(() => true),
|
|
869
|
+
refetch: vi.fn(),
|
|
870
|
+
});
|
|
871
|
+
|
|
696
872
|
renderWithProviders(
|
|
697
873
|
<NavigationMenu
|
|
698
874
|
items={invalidPermissionItems}
|
|
@@ -706,7 +882,7 @@ describe('NavigationMenu Component', () => {
|
|
|
706
882
|
await user.click(trigger);
|
|
707
883
|
|
|
708
884
|
await waitFor(() => {
|
|
709
|
-
// Should not crash and should show the item
|
|
885
|
+
// Should not crash and should show the item (invalid types filtered out, valid permission granted)
|
|
710
886
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
711
887
|
}, { interval: 10 });
|
|
712
888
|
});
|
|
@@ -198,7 +198,10 @@ import { NavigationMenuProps, NavigationItem } from "./types";
|
|
|
198
198
|
import { Button } from "../Button";
|
|
199
199
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../Select";
|
|
200
200
|
import { useUnifiedAuth } from "../../providers/UnifiedAuthProvider";
|
|
201
|
-
import {
|
|
201
|
+
import { useRBAC } from "../../rbac/hooks/useRBAC";
|
|
202
|
+
import { useResolvedScope } from "../../rbac/hooks";
|
|
203
|
+
import { usePermissions } from "../../rbac/hooks/usePermissions";
|
|
204
|
+
import type { Permission, AccessLevel as RBACAccessLevel } from "../../rbac/types";
|
|
202
205
|
|
|
203
206
|
/**
|
|
204
207
|
* Unified NavigationMenu component that supports both dropdown and hierarchical navigation modes.
|
|
@@ -424,10 +427,40 @@ export const NavigationMenu = React.forwardRef<
|
|
|
424
427
|
console.warn('[NavigationMenu] useUnifiedAuth not available, running in unauthenticated mode');
|
|
425
428
|
}
|
|
426
429
|
|
|
430
|
+
// Get RBAC context for permission and role checks
|
|
431
|
+
let rbacContext = null;
|
|
432
|
+
try {
|
|
433
|
+
rbacContext = useRBAC();
|
|
434
|
+
} catch (error) {
|
|
435
|
+
// RBAC not available - permission filtering will be disabled
|
|
436
|
+
console.warn('[NavigationMenu] useRBAC not available, permission filtering disabled');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Get resolved scope for permission checks
|
|
440
|
+
const { supabase } = authContext || {};
|
|
441
|
+
const { selectedOrganisation } = authContext || {};
|
|
442
|
+
const selectedEvent = authContext?.selectedEvent || null;
|
|
443
|
+
const { resolvedScope, isLoading: scopeLoading } = useResolvedScope({
|
|
444
|
+
supabase: supabase || null,
|
|
445
|
+
selectedOrganisationId: selectedOrganisation?.id || null,
|
|
446
|
+
selectedEventId: selectedEvent?.event_id || null
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Get permissions map for synchronous permission checks
|
|
450
|
+
const userId = authContext?.user?.id || '';
|
|
451
|
+
const scope = resolvedScope || { organisationId: '', eventId: undefined, appId: undefined };
|
|
452
|
+
const { permissions: permissionMap, hasAnyPermission, isLoading: permissionsLoading } = usePermissions(
|
|
453
|
+
userId as any,
|
|
454
|
+
scope as any
|
|
455
|
+
);
|
|
456
|
+
|
|
427
457
|
// NEW: Phase 2 - Enhanced Security Features
|
|
428
|
-
// Filter navigation items based on permissions
|
|
458
|
+
// Filter navigation items based on permissions using RBAC hooks
|
|
429
459
|
const filteredItems = React.useMemo(() => {
|
|
430
|
-
|
|
460
|
+
// If filtering disabled, auth unavailable, RBAC unavailable, scope/permissions loading, or no org context: show all items (except hidden)
|
|
461
|
+
if (!filterByPermissions || !authContext || !rbacContext || scopeLoading || permissionsLoading || !resolvedScope?.organisationId) {
|
|
462
|
+
return (items || []).filter(item => !item.meta?.hidden);
|
|
463
|
+
}
|
|
431
464
|
|
|
432
465
|
return (items || []).filter(item => {
|
|
433
466
|
// Check if item should be hidden
|
|
@@ -435,44 +468,98 @@ export const NavigationMenu = React.forwardRef<
|
|
|
435
468
|
|
|
436
469
|
// Check permissions if available
|
|
437
470
|
if (item.permissions && item.permissions.length > 0) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
471
|
+
// Convert string permissions to Permission type and check
|
|
472
|
+
const permissions = item.permissions
|
|
473
|
+
.filter((p): p is string => typeof p === 'string')
|
|
474
|
+
.map(p => p as Permission);
|
|
475
|
+
|
|
476
|
+
if (permissions.length > 0) {
|
|
477
|
+
const hasPermission = hasAnyPermission(permissions);
|
|
478
|
+
if (!hasPermission) return false;
|
|
479
|
+
}
|
|
446
480
|
}
|
|
447
481
|
|
|
448
482
|
// Check roles if available
|
|
449
483
|
if (item.roles && item.roles.length > 0) {
|
|
450
484
|
const hasRole = item.roles.some(role => {
|
|
451
|
-
// Only check string roles, ignore invalid types
|
|
452
485
|
if (typeof role !== 'string') return true;
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
|
|
486
|
+
|
|
487
|
+
// Map role strings to RBAC role checks
|
|
488
|
+
switch (role.toLowerCase()) {
|
|
489
|
+
case 'super_admin':
|
|
490
|
+
case 'super admin':
|
|
491
|
+
return rbacContext.isSuperAdmin;
|
|
492
|
+
case 'org_admin':
|
|
493
|
+
case 'org admin':
|
|
494
|
+
case 'admin':
|
|
495
|
+
return rbacContext.isOrgAdmin || rbacContext.isSuperAdmin;
|
|
496
|
+
case 'event_admin':
|
|
497
|
+
case 'event admin':
|
|
498
|
+
return rbacContext.isEventAdmin || rbacContext.isSuperAdmin;
|
|
499
|
+
default:
|
|
500
|
+
// For other roles, check against organisationRole or eventAppRole
|
|
501
|
+
return (
|
|
502
|
+
rbacContext.organisationRole === role ||
|
|
503
|
+
rbacContext.eventAppRole === role ||
|
|
504
|
+
rbacContext.isSuperAdmin
|
|
505
|
+
);
|
|
506
|
+
}
|
|
456
507
|
});
|
|
457
508
|
if (!hasRole) return false;
|
|
458
509
|
}
|
|
459
510
|
|
|
460
511
|
// Check access level if available
|
|
461
512
|
if (item.accessLevel) {
|
|
462
|
-
// Only check string access levels, ignore invalid types
|
|
463
513
|
if (typeof item.accessLevel === 'string') {
|
|
464
|
-
//
|
|
465
|
-
const accessLevel = item.accessLevel as
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (
|
|
514
|
+
// Map access level string to RBAC access level
|
|
515
|
+
const accessLevel = item.accessLevel.toLowerCase() as RBACAccessLevel;
|
|
516
|
+
const userEventRole = rbacContext.eventAppRole;
|
|
517
|
+
|
|
518
|
+
// If user is super admin, they have all access levels
|
|
519
|
+
if (rbacContext.isSuperAdmin) {
|
|
520
|
+
// Super admin has access
|
|
521
|
+
} else {
|
|
522
|
+
// Map eventAppRole to access level for comparison
|
|
523
|
+
// eventAppRole: 'viewer' | 'participant' | 'planner' | 'event_admin'
|
|
524
|
+
const roleToAccessLevel: Record<string, RBACAccessLevel> = {
|
|
525
|
+
'viewer': 'viewer',
|
|
526
|
+
'participant': 'participant',
|
|
527
|
+
'planner': 'planner',
|
|
528
|
+
'event_admin': 'admin',
|
|
529
|
+
};
|
|
530
|
+
const userAccessLevel = userEventRole ? (roleToAccessLevel[userEventRole] || 'viewer') : null;
|
|
531
|
+
|
|
532
|
+
// Check if user's access level meets the required access level
|
|
533
|
+
const levelHierarchy: Record<RBACAccessLevel, number> = {
|
|
534
|
+
viewer: 1,
|
|
535
|
+
participant: 2,
|
|
536
|
+
planner: 3,
|
|
537
|
+
admin: 4,
|
|
538
|
+
super: 5
|
|
539
|
+
};
|
|
540
|
+
const requiredLevel = levelHierarchy[accessLevel] || 0;
|
|
541
|
+
const userLevel = userAccessLevel ? levelHierarchy[userAccessLevel] || 0 : 0;
|
|
542
|
+
|
|
543
|
+
if (userLevel < requiredLevel) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
470
547
|
}
|
|
471
548
|
}
|
|
472
549
|
|
|
473
550
|
return true;
|
|
474
551
|
});
|
|
475
|
-
}, [
|
|
552
|
+
}, [
|
|
553
|
+
items,
|
|
554
|
+
filterByPermissions,
|
|
555
|
+
authContext,
|
|
556
|
+
rbacContext,
|
|
557
|
+
permissionMap,
|
|
558
|
+
hasAnyPermission,
|
|
559
|
+
scopeLoading,
|
|
560
|
+
permissionsLoading,
|
|
561
|
+
resolvedScope
|
|
562
|
+
]);
|
|
476
563
|
|
|
477
564
|
// Log navigation access attempts for debugging
|
|
478
565
|
React.useEffect(() => {
|
|
@@ -552,26 +639,46 @@ export const NavigationMenu = React.forwardRef<
|
|
|
552
639
|
// Check if item should be visible (already filtered)
|
|
553
640
|
const isItemVisible = filteredItems.some(filtered => filtered.id === item.id);
|
|
554
641
|
|
|
555
|
-
// Check permissions if the item requires them
|
|
642
|
+
// Check permissions if the item requires them using RBAC hooks
|
|
556
643
|
let hasPermission = true; // Default to true if no permission requirements
|
|
557
644
|
|
|
558
|
-
if (item.permissions && item.permissions.length > 0) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
645
|
+
if (item.permissions && item.permissions.length > 0 && rbacContext && hasAnyPermission) {
|
|
646
|
+
// Convert string permissions to Permission type and check
|
|
647
|
+
const permissions = item.permissions
|
|
648
|
+
.filter((p): p is string => typeof p === 'string')
|
|
649
|
+
.map(p => p as Permission);
|
|
650
|
+
|
|
651
|
+
if (permissions.length > 0) {
|
|
652
|
+
hasPermission = hasAnyPermission(permissions);
|
|
653
|
+
}
|
|
565
654
|
}
|
|
566
655
|
|
|
567
|
-
if (!hasPermission) {
|
|
656
|
+
if (!hasPermission && rbacContext) {
|
|
568
657
|
// Check roles if permissions check failed or item has role requirements
|
|
569
658
|
if (item.roles && item.roles.length > 0) {
|
|
570
659
|
hasPermission = item.roles.some(role => {
|
|
571
660
|
if (typeof role !== 'string') return true;
|
|
572
|
-
|
|
573
|
-
// RBAC
|
|
574
|
-
|
|
661
|
+
|
|
662
|
+
// Map role strings to RBAC role checks (same logic as filtering)
|
|
663
|
+
switch (role.toLowerCase()) {
|
|
664
|
+
case 'super_admin':
|
|
665
|
+
case 'super admin':
|
|
666
|
+
return rbacContext.isSuperAdmin;
|
|
667
|
+
case 'org_admin':
|
|
668
|
+
case 'org admin':
|
|
669
|
+
case 'admin':
|
|
670
|
+
return rbacContext.isOrgAdmin || rbacContext.isSuperAdmin;
|
|
671
|
+
case 'event_admin':
|
|
672
|
+
case 'event admin':
|
|
673
|
+
return rbacContext.isEventAdmin || rbacContext.isSuperAdmin;
|
|
674
|
+
default:
|
|
675
|
+
// For other roles, check against organisationRole or eventAppRole
|
|
676
|
+
return (
|
|
677
|
+
rbacContext.organisationRole === role ||
|
|
678
|
+
rbacContext.eventAppRole === role ||
|
|
679
|
+
rbacContext.isSuperAdmin
|
|
680
|
+
);
|
|
681
|
+
}
|
|
575
682
|
});
|
|
576
683
|
}
|
|
577
684
|
}
|
|
@@ -48,12 +48,12 @@ vi.mock('../../../hooks/useAppConfig', () => ({
|
|
|
48
48
|
|
|
49
49
|
// Mock the FileDisplay component
|
|
50
50
|
vi.mock('../../FileDisplay/FileDisplay', () => ({
|
|
51
|
-
FileDisplay: vi.fn(({ table_name, record_id, organisation_id, category, className }) => (
|
|
51
|
+
FileDisplay: vi.fn(({ table_name, record_id, organisation_id, category, className, size = 'md' }) => (
|
|
52
52
|
<div
|
|
53
53
|
data-testid="event-logo"
|
|
54
|
-
data-
|
|
55
|
-
data-
|
|
56
|
-
data-organisation-id={
|
|
54
|
+
data-table-name={table_name}
|
|
55
|
+
data-record-id={record_id}
|
|
56
|
+
data-organisation-id={organisation_id}
|
|
57
57
|
data-size={size}
|
|
58
58
|
className={className}
|
|
59
59
|
>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Customizable positioning and styling
|
|
13
13
|
* - Swipe gestures for dismissal
|
|
14
14
|
* - Keyboard navigation support
|
|
15
|
-
* - Auto-dismiss with
|
|
15
|
+
* - Auto-dismiss with default 10 second duration (configurable)
|
|
16
16
|
* - Action buttons and close functionality
|
|
17
17
|
* - Responsive design
|
|
18
18
|
* - Accessibility compliant
|
|
@@ -169,24 +169,34 @@ export function usePublicFileDisplay(
|
|
|
169
169
|
throw new Error(rpcError.message || 'Failed to fetch file reference');
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
// RPC returns partial data
|
|
172
|
+
// RPC returns partial data with: id, file_path, file_metadata, is_public, created_at
|
|
173
|
+
// We have table_name, record_id, organisation_id from function parameters
|
|
174
|
+
// We can construct FileReference objects directly without another query (avoiding RLS issues)
|
|
173
175
|
if (!data || data.length === 0) {
|
|
174
176
|
files = [];
|
|
175
177
|
} else {
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
// Construct file reference objects from RPC response
|
|
179
|
+
// This avoids RLS issues - the RPC already validated permissions and filtered public files
|
|
180
|
+
files = data
|
|
181
|
+
.filter((item: any) => {
|
|
182
|
+
// RPC should only return public files for public context, but verify
|
|
183
|
+
return item.is_public === true && item.id && item.file_path && item.file_metadata;
|
|
184
|
+
})
|
|
185
|
+
.map((item: any) => {
|
|
186
|
+
// Construct complete file reference from RPC response + function parameters
|
|
187
|
+
return {
|
|
188
|
+
id: item.id,
|
|
189
|
+
table_name: table_name,
|
|
190
|
+
record_id: record_id,
|
|
191
|
+
file_path: item.file_path,
|
|
192
|
+
file_metadata: item.file_metadata || {},
|
|
193
|
+
organisation_id: organisation_id,
|
|
194
|
+
app_id: item.file_metadata?.app_id || null,
|
|
195
|
+
is_public: true, // RPC already filtered for public files
|
|
196
|
+
created_at: item.created_at || new Date().toISOString(),
|
|
197
|
+
updated_at: item.created_at || new Date().toISOString()
|
|
198
|
+
};
|
|
199
|
+
});
|
|
190
200
|
}
|
|
191
201
|
} else {
|
|
192
202
|
// Multiple files mode - use RPC to get all files
|
|
@@ -23,6 +23,17 @@ vi.mock('../theming/runtime', () => ({
|
|
|
23
23
|
clearPalette: vi.fn()
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
+
// Mock react-router-dom useLocation
|
|
27
|
+
vi.mock('react-router-dom', () => ({
|
|
28
|
+
useLocation: vi.fn(() => ({
|
|
29
|
+
pathname: '/',
|
|
30
|
+
search: '',
|
|
31
|
+
hash: '',
|
|
32
|
+
state: null,
|
|
33
|
+
key: 'default'
|
|
34
|
+
}))
|
|
35
|
+
}));
|
|
36
|
+
|
|
26
37
|
describe('useEventTheme', () => {
|
|
27
38
|
const mockUseEvents = vi.mocked(useEvents);
|
|
28
39
|
const mockApplyPalette = vi.mocked(applyPalette);
|
|
@@ -224,12 +224,21 @@ export function useFileDisplay(
|
|
|
224
224
|
if (category && files.length > 0) {
|
|
225
225
|
// Single file mode - get first file
|
|
226
226
|
const firstFile = files[0];
|
|
227
|
+
console.log('[useFileDisplay] Processing category files, first file:', {
|
|
228
|
+
id: firstFile.id,
|
|
229
|
+
file_path: firstFile.file_path,
|
|
230
|
+
is_public: firstFile.is_public,
|
|
231
|
+
has_file_metadata: !!firstFile.file_metadata,
|
|
232
|
+
category_in_metadata: firstFile.file_metadata?.category
|
|
233
|
+
});
|
|
234
|
+
|
|
227
235
|
setFileReference(firstFile);
|
|
228
236
|
|
|
229
237
|
// Generate URL based on file visibility
|
|
230
238
|
let url: string | null = null;
|
|
231
239
|
if (firstFile.is_public) {
|
|
232
240
|
url = getPublicUrl(supabase, firstFile.file_path, true);
|
|
241
|
+
console.log('[useFileDisplay] Generated public URL:', url);
|
|
233
242
|
} else {
|
|
234
243
|
const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
|
|
235
244
|
appName: 'pace-core',
|
|
@@ -237,7 +246,9 @@ export function useFileDisplay(
|
|
|
237
246
|
expiresIn: 3600
|
|
238
247
|
});
|
|
239
248
|
url = signedUrlResult?.url || null;
|
|
249
|
+
console.log('[useFileDisplay] Generated signed URL:', url ? 'URL generated' : 'URL generation failed');
|
|
240
250
|
}
|
|
251
|
+
console.log('[useFileDisplay] Setting file URL:', url ? 'URL set' : 'URL is null');
|
|
241
252
|
setFileUrl(url);
|
|
242
253
|
} else {
|
|
243
254
|
// Multiple files mode - generate URLs for all files
|
|
@@ -152,12 +152,28 @@ describe('useSecureDataAccess', () => {
|
|
|
152
152
|
expect(freshMockQueryBuilder.select).toHaveBeenCalledWith('*');
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
it
|
|
156
|
-
|
|
157
|
-
// The mock needs to properly support: .from().select().eq().eq().range().then()
|
|
158
|
-
// This is a complex integration test that would require refactoring the Supabase mock
|
|
159
|
-
// See issue: Need comprehensive range() support in createMockQueryBuilder
|
|
155
|
+
it('executes secure query with pagination', async () => {
|
|
156
|
+
const paginatedData = Array.from({ length: 10 }, (_, i) => ({ id: `record-${i + 20}` }));
|
|
160
157
|
|
|
158
|
+
// Ensure range() returns a thenable that resolves with paginated data
|
|
159
|
+
// and that all chain methods (eq, select, etc.) maintain the range function
|
|
160
|
+
freshMockQueryBuilder.range = vi.fn().mockImplementation(function(min: number, max: number) {
|
|
161
|
+
const rangedBuilder = Object.assign({}, this, {
|
|
162
|
+
then: vi.fn().mockImplementation((resolve) => {
|
|
163
|
+
resolve({ data: paginatedData, error: null });
|
|
164
|
+
}),
|
|
165
|
+
catch: vi.fn(),
|
|
166
|
+
finally: vi.fn(),
|
|
167
|
+
range: freshMockQueryBuilder.range // Maintain range for further chaining if needed
|
|
168
|
+
});
|
|
169
|
+
return rangedBuilder;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Ensure eq() returns this which has range()
|
|
173
|
+
freshMockQueryBuilder.eq = vi.fn().mockReturnThis();
|
|
174
|
+
freshMockQueryBuilder.select = vi.fn().mockReturnThis();
|
|
175
|
+
freshMockQueryBuilder.limit = vi.fn().mockReturnThis();
|
|
176
|
+
|
|
161
177
|
const { result } = renderHook(() => useSecureDataAccess());
|
|
162
178
|
|
|
163
179
|
const data = await result.current.secureQuery('users', '*', {}, {
|
|
@@ -167,6 +183,7 @@ describe('useSecureDataAccess', () => {
|
|
|
167
183
|
|
|
168
184
|
expect(freshMockQueryBuilder.select).toHaveBeenCalledWith('*');
|
|
169
185
|
expect(freshMockQueryBuilder.range).toHaveBeenCalledWith(20, 29); // offset to (offset + limit - 1)
|
|
186
|
+
expect(data).toEqual(paginatedData);
|
|
170
187
|
});
|
|
171
188
|
|
|
172
189
|
it('handles query errors gracefully', async () => {
|