@jmruthers/pace-core 0.5.110 → 0.5.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-D3BK2FCN.js → DataTable-3D3BUZDV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-KZZUO27W.js} +3 -3
- package/dist/{api-PIE4JRFS.js → api-QPMBZZUZ.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-H4YJJF7R.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-3OGQLOJM.js} +23 -7
- package/dist/chunk-3OGQLOJM.js.map +1 -0
- package/dist/{chunk-EYSXQ756.js → chunk-7H75SHXZ.js} +2 -2
- package/dist/{chunk-D6MEKC27.js → chunk-BUN7NMV7.js} +2 -2
- package/dist/{chunk-AWK2FAUN.js → chunk-C5RN4TE5.js} +7 -7
- package/dist/{chunk-3J5N2T2N.js → chunk-EKVVTPIF.js} +183 -127
- package/dist/chunk-EKVVTPIF.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-F6QB26OS.js} +290 -255
- package/dist/chunk-F6QB26OS.js.map +1 -0
- package/dist/{chunk-HADXAZT3.js → chunk-I7JC7PTJ.js} +54 -92
- package/dist/chunk-I7JC7PTJ.js.map +1 -0
- package/dist/{chunk-EZ64QG2I.js → chunk-L36JW4KV.js} +2 -2
- package/dist/{chunk-7GBEBJLR.js → chunk-MNSGWRPB.js} +45 -37
- package/dist/chunk-MNSGWRPB.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-NEONKMTU.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-OO3V7W4H.js} +35 -11
- package/dist/chunk-OO3V7W4H.js.map +1 -0
- package/dist/{chunk-XRSP3H52.js → chunk-TAJRS6YB.js} +57 -23
- package/dist/chunk-TAJRS6YB.js.map +1 -0
- package/dist/{chunk-HGZSO43Y.js → chunk-WMPZY26G.js} +8 -4
- package/dist/{chunk-HGZSO43Y.js.map → chunk-WMPZY26G.js.map} +1 -1
- 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 +13 -8
- 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 +4 -4
- 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 +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- 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 +36 -36
- 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/migration/rbac-migration.md +65 -66
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +3 -3
- package/docs/rbac/troubleshooting.md +2 -1
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.test.tsx +405 -154
- package/src/components/DataTable/components/DataTableCore.tsx +6 -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/EventSelector/EventSelector.tsx +32 -2
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +56 -8
- package/src/components/NavigationMenu/NavigationMenu.tsx +75 -12
- 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 +37 -13
- package/src/rbac/api.ts +25 -8
- package/src/rbac/audit-enhanced.ts +14 -2
- package/src/rbac/audit.test.ts +18 -8
- package/src/rbac/audit.ts +25 -6
- 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 +4 -3
- 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 +1 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +12 -2
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +65 -25
- 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 +22 -7
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +42 -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-3J5N2T2N.js.map +0 -1
- package/dist/chunk-7GBEBJLR.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-HADXAZT3.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-XRSP3H52.js.map +0 -1
- /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-3D3BUZDV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-KZZUO27W.js.map} +0 -0
- /package/dist/{api-PIE4JRFS.js.map → api-QPMBZZUZ.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-H4YJJF7R.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-7H75SHXZ.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-BUN7NMV7.js.map} +0 -0
- /package/dist/{chunk-AWK2FAUN.js.map → chunk-C5RN4TE5.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-L36JW4KV.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-NEONKMTU.js.map} +0 -0
|
@@ -268,8 +268,8 @@ describe('usePermissions Hook', () => {
|
|
|
268
268
|
'create:users': true,
|
|
269
269
|
'update:users': true,
|
|
270
270
|
'delete:users': true,
|
|
271
|
-
'
|
|
272
|
-
'
|
|
271
|
+
'update:organisations': true,
|
|
272
|
+
'update:events': true,
|
|
273
273
|
'super_admin': true
|
|
274
274
|
};
|
|
275
275
|
|
|
@@ -287,7 +287,7 @@ describe('usePermissions Hook', () => {
|
|
|
287
287
|
expect(result.current.hasPermission('create:users')).toBe(true);
|
|
288
288
|
expect(result.current.hasPermission('update:users')).toBe(true);
|
|
289
289
|
expect(result.current.hasPermission('delete:users')).toBe(true);
|
|
290
|
-
expect(result.current.hasPermission('
|
|
290
|
+
expect(result.current.hasPermission('update:organisations')).toBe(true);
|
|
291
291
|
expect(result.current.hasPermission('super_admin')).toBe(true);
|
|
292
292
|
});
|
|
293
293
|
|
|
@@ -297,8 +297,8 @@ describe('usePermissions Hook', () => {
|
|
|
297
297
|
'create:users': true,
|
|
298
298
|
'update:users': true,
|
|
299
299
|
'delete:users': false, // Org admin can't delete users
|
|
300
|
-
'
|
|
301
|
-
'
|
|
300
|
+
'update:organisations': true,
|
|
301
|
+
'update:events': true,
|
|
302
302
|
'org_admin': true
|
|
303
303
|
};
|
|
304
304
|
|
|
@@ -316,7 +316,7 @@ describe('usePermissions Hook', () => {
|
|
|
316
316
|
expect(result.current.hasPermission('create:users')).toBe(true);
|
|
317
317
|
expect(result.current.hasPermission('update:users')).toBe(true);
|
|
318
318
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
319
|
-
expect(result.current.hasPermission('
|
|
319
|
+
expect(result.current.hasPermission('update:organisations')).toBe(true);
|
|
320
320
|
expect(result.current.hasPermission('org_admin')).toBe(true);
|
|
321
321
|
});
|
|
322
322
|
|
|
@@ -326,8 +326,8 @@ describe('usePermissions Hook', () => {
|
|
|
326
326
|
'create:users': false,
|
|
327
327
|
'update:users': false,
|
|
328
328
|
'delete:users': false,
|
|
329
|
-
'
|
|
330
|
-
'
|
|
329
|
+
'update:organisations': false,
|
|
330
|
+
'update:events': true,
|
|
331
331
|
'event_admin': true
|
|
332
332
|
};
|
|
333
333
|
|
|
@@ -345,8 +345,8 @@ describe('usePermissions Hook', () => {
|
|
|
345
345
|
expect(result.current.hasPermission('create:users')).toBe(false);
|
|
346
346
|
expect(result.current.hasPermission('update:users')).toBe(false);
|
|
347
347
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
348
|
-
expect(result.current.hasPermission('
|
|
349
|
-
expect(result.current.hasPermission('
|
|
348
|
+
expect(result.current.hasPermission('update:organisations')).toBe(false);
|
|
349
|
+
expect(result.current.hasPermission('update:events')).toBe(true);
|
|
350
350
|
expect(result.current.hasPermission('event_admin')).toBe(true);
|
|
351
351
|
});
|
|
352
352
|
|
|
@@ -356,8 +356,8 @@ describe('usePermissions Hook', () => {
|
|
|
356
356
|
'create:users': false,
|
|
357
357
|
'update:users': false,
|
|
358
358
|
'delete:users': false,
|
|
359
|
-
'
|
|
360
|
-
'
|
|
359
|
+
'update:organisations': false,
|
|
360
|
+
'update:events': false,
|
|
361
361
|
'member': true
|
|
362
362
|
};
|
|
363
363
|
|
|
@@ -375,8 +375,8 @@ describe('usePermissions Hook', () => {
|
|
|
375
375
|
expect(result.current.hasPermission('create:users')).toBe(false);
|
|
376
376
|
expect(result.current.hasPermission('update:users')).toBe(false);
|
|
377
377
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
378
|
-
expect(result.current.hasPermission('
|
|
379
|
-
expect(result.current.hasPermission('
|
|
378
|
+
expect(result.current.hasPermission('update:organisations')).toBe(false);
|
|
379
|
+
expect(result.current.hasPermission('update:events')).toBe(false);
|
|
380
380
|
expect(result.current.hasPermission('member')).toBe(true);
|
|
381
381
|
});
|
|
382
382
|
|
|
@@ -386,8 +386,8 @@ describe('usePermissions Hook', () => {
|
|
|
386
386
|
'create:users': false,
|
|
387
387
|
'update:users': false,
|
|
388
388
|
'delete:users': false,
|
|
389
|
-
'
|
|
390
|
-
'
|
|
389
|
+
'update:organisations': false,
|
|
390
|
+
'update:events': false,
|
|
391
391
|
'participant': true
|
|
392
392
|
};
|
|
393
393
|
|
|
@@ -405,8 +405,8 @@ describe('usePermissions Hook', () => {
|
|
|
405
405
|
expect(result.current.hasPermission('create:users')).toBe(false);
|
|
406
406
|
expect(result.current.hasPermission('update:users')).toBe(false);
|
|
407
407
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
408
|
-
expect(result.current.hasPermission('
|
|
409
|
-
expect(result.current.hasPermission('
|
|
408
|
+
expect(result.current.hasPermission('update:organisations')).toBe(false);
|
|
409
|
+
expect(result.current.hasPermission('update:events')).toBe(false);
|
|
410
410
|
expect(result.current.hasPermission('participant')).toBe(true);
|
|
411
411
|
});
|
|
412
412
|
|
|
@@ -416,8 +416,8 @@ describe('usePermissions Hook', () => {
|
|
|
416
416
|
'create:users': false,
|
|
417
417
|
'update:users': true,
|
|
418
418
|
'delete:users': false,
|
|
419
|
-
'
|
|
420
|
-
'
|
|
419
|
+
'update:organisations': false,
|
|
420
|
+
'update:events': true
|
|
421
421
|
};
|
|
422
422
|
|
|
423
423
|
mockGetPermissionMap.mockResolvedValue(mixedPermissions);
|
|
@@ -432,8 +432,8 @@ describe('usePermissions Hook', () => {
|
|
|
432
432
|
// Test hasAnyPermission with mixed results
|
|
433
433
|
expect(result.current.hasAnyPermission(['read:users', 'create:users'])).toBe(true); // One is true
|
|
434
434
|
expect(result.current.hasAnyPermission(['create:users', 'delete:users'])).toBe(false); // Both are false
|
|
435
|
-
expect(result.current.hasAnyPermission(['update:users', '
|
|
436
|
-
expect(result.current.hasAnyPermission(['read:users', 'update:users', '
|
|
435
|
+
expect(result.current.hasAnyPermission(['update:users', 'update:events'])).toBe(true); // Both are true
|
|
436
|
+
expect(result.current.hasAnyPermission(['read:users', 'update:users', 'update:events'])).toBe(true); // All are true
|
|
437
437
|
});
|
|
438
438
|
|
|
439
439
|
it('handles mixed permission scenarios with hasAllPermissions', async () => {
|
|
@@ -442,8 +442,8 @@ describe('usePermissions Hook', () => {
|
|
|
442
442
|
'create:users': false,
|
|
443
443
|
'update:users': true,
|
|
444
444
|
'delete:users': false,
|
|
445
|
-
'
|
|
446
|
-
'
|
|
445
|
+
'update:organisations': false,
|
|
446
|
+
'update:events': true
|
|
447
447
|
};
|
|
448
448
|
|
|
449
449
|
mockGetPermissionMap.mockResolvedValue(mixedPermissions);
|
|
@@ -459,7 +459,7 @@ describe('usePermissions Hook', () => {
|
|
|
459
459
|
expect(result.current.hasAllPermissions(['read:users', 'update:users'])).toBe(true); // Both are true
|
|
460
460
|
expect(result.current.hasAllPermissions(['read:users', 'create:users'])).toBe(false); // One is false
|
|
461
461
|
expect(result.current.hasAllPermissions(['create:users', 'delete:users'])).toBe(false); // Both are false
|
|
462
|
-
expect(result.current.hasAllPermissions(['read:users', 'update:users', '
|
|
462
|
+
expect(result.current.hasAllPermissions(['read:users', 'update:users', 'update:events'])).toBe(true); // All are true
|
|
463
463
|
});
|
|
464
464
|
});
|
|
465
465
|
|
|
@@ -52,6 +52,22 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
52
52
|
const [error, setError] = useState<Error | null>(null);
|
|
53
53
|
const isFetchingRef = useRef(false);
|
|
54
54
|
|
|
55
|
+
// Add timeout for missing organisation context (3 seconds)
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
58
|
+
const timeoutId = setTimeout(() => {
|
|
59
|
+
setError(new Error('Organisation context is required for permission checks'));
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
62
|
+
|
|
63
|
+
return () => clearTimeout(timeoutId);
|
|
64
|
+
}
|
|
65
|
+
// Clear error if organisation context becomes available
|
|
66
|
+
if (error?.message === 'Organisation context is required for permission checks') {
|
|
67
|
+
setError(null);
|
|
68
|
+
}
|
|
69
|
+
}, [scope.organisationId, error]);
|
|
70
|
+
|
|
55
71
|
useEffect(() => {
|
|
56
72
|
const fetchPermissions = async () => {
|
|
57
73
|
// Prevent multiple simultaneous fetches
|
|
@@ -65,10 +81,11 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
65
81
|
return;
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
// Don't fetch permissions if scope is invalid (e.g., organisationId is empty)
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
// Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
85
|
+
// Wait for organisation context to resolve
|
|
86
|
+
// IMPORTANT: Don't clear existing permissions here - keep them until we have new ones
|
|
87
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
88
|
+
// Keep existing permissions, just mark as loading
|
|
72
89
|
setIsLoading(true);
|
|
73
90
|
setError(null);
|
|
74
91
|
return;
|
|
@@ -79,10 +96,17 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
79
96
|
setIsLoading(true);
|
|
80
97
|
setError(null);
|
|
81
98
|
|
|
99
|
+
// Fetch new permissions - don't clear old ones until we have new ones
|
|
82
100
|
const permissionMap = await getPermissionMap({ userId, scope });
|
|
101
|
+
|
|
102
|
+
// Only update permissions if fetch was successful
|
|
83
103
|
setPermissions(permissionMap);
|
|
84
104
|
} catch (err) {
|
|
105
|
+
// On error, keep existing permissions but set error state
|
|
106
|
+
// This prevents the UI from losing all items when there's a transient error
|
|
107
|
+
console.error('[usePermissions] Failed to fetch permissions:', err);
|
|
85
108
|
setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
|
|
109
|
+
// Don't clear permissions on error - keep what we had
|
|
86
110
|
} finally {
|
|
87
111
|
setIsLoading(false);
|
|
88
112
|
isFetchingRef.current = false;
|
|
@@ -125,9 +149,10 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
125
149
|
return;
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
// Don't fetch permissions if scope is invalid (e.g., organisationId is empty)
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
// Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
153
|
+
// IMPORTANT: Don't clear existing permissions - keep them until we have new ones
|
|
154
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
155
|
+
// Keep existing permissions, just mark as loading
|
|
131
156
|
setIsLoading(true);
|
|
132
157
|
setError(null);
|
|
133
158
|
return;
|
|
@@ -138,10 +163,17 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
138
163
|
setIsLoading(true);
|
|
139
164
|
setError(null);
|
|
140
165
|
|
|
166
|
+
// Fetch new permissions - don't clear old ones until we have new ones
|
|
141
167
|
const permissionMap = await getPermissionMap({ userId, scope });
|
|
168
|
+
|
|
169
|
+
// Only update permissions if fetch was successful
|
|
142
170
|
setPermissions(permissionMap);
|
|
143
171
|
} catch (err) {
|
|
172
|
+
// On error, keep existing permissions but set error state
|
|
173
|
+
// This prevents the UI from losing all items when there's a transient error
|
|
174
|
+
console.error('[usePermissions] Failed to refetch permissions:', err);
|
|
144
175
|
setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
|
|
176
|
+
// Don't clear permissions on error - keep what we had
|
|
145
177
|
} finally {
|
|
146
178
|
setIsLoading(false);
|
|
147
179
|
isFetchingRef.current = false;
|
|
@@ -193,6 +225,23 @@ export function useCan(
|
|
|
193
225
|
const [isLoading, setIsLoading] = useState(true);
|
|
194
226
|
const [error, setError] = useState<Error | null>(null);
|
|
195
227
|
|
|
228
|
+
// Add timeout for missing organisation context (3 seconds)
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
231
|
+
const timeoutId = setTimeout(() => {
|
|
232
|
+
setError(new Error('Organisation context is required for permission checks'));
|
|
233
|
+
setIsLoading(false);
|
|
234
|
+
setCan(false);
|
|
235
|
+
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
236
|
+
|
|
237
|
+
return () => clearTimeout(timeoutId);
|
|
238
|
+
}
|
|
239
|
+
// Clear error if organisation context becomes available
|
|
240
|
+
if (error?.message === 'Organisation context is required for permission checks') {
|
|
241
|
+
setError(null);
|
|
242
|
+
}
|
|
243
|
+
}, [scope.organisationId, error]);
|
|
244
|
+
|
|
196
245
|
// Use refs to track the last values to prevent unnecessary re-runs
|
|
197
246
|
const lastUserIdRef = useRef<UUID | null>(null);
|
|
198
247
|
const lastScopeRef = useRef<string | null>(null);
|
|
@@ -226,12 +275,13 @@ export function useCan(
|
|
|
226
275
|
return;
|
|
227
276
|
}
|
|
228
277
|
|
|
229
|
-
// Don't check permissions if scope is invalid (e.g., organisationId is empty)
|
|
230
|
-
//
|
|
231
|
-
if (!scope.organisationId || scope.organisationId.trim() === '') {
|
|
232
|
-
console.log('[useCan] Invalid scope - organisationId is empty:', { scope, permission, pageId });
|
|
233
|
-
setCan(false);
|
|
278
|
+
// Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
279
|
+
// Wait for organisation context to resolve
|
|
280
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
234
281
|
setIsLoading(true);
|
|
282
|
+
setCan(false);
|
|
283
|
+
setError(null);
|
|
284
|
+
// Timeout is handled in separate useEffect (Phase 1.4)
|
|
235
285
|
return;
|
|
236
286
|
}
|
|
237
287
|
|
|
@@ -239,21 +289,10 @@ export function useCan(
|
|
|
239
289
|
setIsLoading(true);
|
|
240
290
|
setError(null);
|
|
241
291
|
|
|
242
|
-
console.log('[useCan] Checking permission:', {
|
|
243
|
-
userId,
|
|
244
|
-
organisationId: scope.organisationId,
|
|
245
|
-
eventId: scope.eventId,
|
|
246
|
-
appId: scope.appId,
|
|
247
|
-
permission,
|
|
248
|
-
pageId
|
|
249
|
-
});
|
|
250
|
-
|
|
251
292
|
const result = useCache
|
|
252
293
|
? await isPermittedCached({ userId, scope, permission, pageId })
|
|
253
294
|
: await isPermitted({ userId, scope, permission, pageId });
|
|
254
295
|
|
|
255
|
-
console.log('[useCan] Permission check result:', { permission, result, pageId });
|
|
256
|
-
|
|
257
296
|
setCan(result);
|
|
258
297
|
} catch (err) {
|
|
259
298
|
console.error('[useCan] Permission check error:', { permission, error: err });
|
|
@@ -275,10 +314,11 @@ export function useCan(
|
|
|
275
314
|
return;
|
|
276
315
|
}
|
|
277
316
|
|
|
278
|
-
// Don't check permissions if scope is invalid (e.g., organisationId is empty)
|
|
279
|
-
if (!scope.organisationId || scope.organisationId.trim() === '') {
|
|
317
|
+
// Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
318
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
280
319
|
setCan(false);
|
|
281
320
|
setIsLoading(true);
|
|
321
|
+
setError(null);
|
|
282
322
|
return;
|
|
283
323
|
}
|
|
284
324
|
|
|
@@ -57,7 +57,6 @@ vi.mock('./useRBAC', () => ({
|
|
|
57
57
|
eventAppRole: 'participant',
|
|
58
58
|
isSuperAdmin: false,
|
|
59
59
|
isOrgAdmin: false,
|
|
60
|
-
hasPermission: () => true,
|
|
61
60
|
hasGlobalPermission: () => true,
|
|
62
61
|
error: null,
|
|
63
62
|
}),
|
|
@@ -80,18 +79,12 @@ describe('useRBAC Hook - Simple Tests', () => {
|
|
|
80
79
|
expect(result.current).toHaveProperty('globalRole');
|
|
81
80
|
expect(result.current).toHaveProperty('organisationRole');
|
|
82
81
|
expect(result.current).toHaveProperty('eventAppRole');
|
|
83
|
-
expect(result.current).toHaveProperty('hasPermission');
|
|
84
82
|
expect(result.current).toHaveProperty('hasGlobalPermission');
|
|
85
83
|
expect(result.current).toHaveProperty('isSuperAdmin');
|
|
86
84
|
expect(result.current).toHaveProperty('isOrgAdmin');
|
|
87
85
|
expect(result.current).toHaveProperty('isLoading');
|
|
88
86
|
expect(result.current).toHaveProperty('error');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
it('hasPermission is a function', () => {
|
|
92
|
-
const { result } = renderHook(() => useRBAC());
|
|
93
|
-
|
|
94
|
-
expect(typeof result.current.hasPermission).toBe('function');
|
|
87
|
+
// Note: hasPermission was removed - use useCan() hook instead
|
|
95
88
|
});
|
|
96
89
|
|
|
97
90
|
it('hasGlobalPermission is a function', () => {
|
|
@@ -97,7 +97,7 @@ describe('useRBAC', () => {
|
|
|
97
97
|
|
|
98
98
|
await waitFor(() => {
|
|
99
99
|
expect(result.current.isLoading).toBe(false);
|
|
100
|
-
expect(result.current.
|
|
100
|
+
expect(result.current.hasGlobalPermission).toBeTypeOf('function');
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
expect(mockResolveAppContext).toHaveBeenCalledWith({ userId: 'user-1', appName: 'test-app' });
|
|
@@ -109,45 +109,8 @@ describe('useRBAC', () => {
|
|
|
109
109
|
);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
user: { id: 'user-1' },
|
|
115
|
-
session: { access_token: 'token' },
|
|
116
|
-
appName: 'test-app',
|
|
117
|
-
});
|
|
118
|
-
mockUseOrganisations.mockReturnValue({ selectedOrganisation: { id: 'org-1' } });
|
|
119
|
-
mockGetPermissionMap.mockResolvedValue({ 'read:dashboard': true });
|
|
120
|
-
|
|
121
|
-
const { result } = renderHook(() => useRBAC('dashboard'));
|
|
122
|
-
|
|
123
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
124
|
-
|
|
125
|
-
mockIsPermittedCached.mockClear();
|
|
126
|
-
const allowed = await result.current.hasPermission('read', 'dashboard');
|
|
127
|
-
|
|
128
|
-
expect(allowed).toBe(true);
|
|
129
|
-
expect(mockIsPermittedCached).not.toHaveBeenCalled();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('falls back to engine when permission not cached', async () => {
|
|
133
|
-
mockUseUnifiedAuth.mockReturnValue({
|
|
134
|
-
user: { id: 'user-1' },
|
|
135
|
-
session: { access_token: 'token' },
|
|
136
|
-
appName: 'test-app',
|
|
137
|
-
});
|
|
138
|
-
mockUseOrganisations.mockReturnValue({ selectedOrganisation: { id: 'org-1' } });
|
|
139
|
-
mockGetPermissionMap.mockResolvedValue({});
|
|
140
|
-
mockIsPermittedCached.mockResolvedValue(true);
|
|
141
|
-
|
|
142
|
-
const { result } = renderHook(() => useRBAC('dashboard'));
|
|
143
|
-
|
|
144
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
145
|
-
|
|
146
|
-
const allowed = await result.current.hasPermission('read', 'dashboard');
|
|
147
|
-
|
|
148
|
-
expect(allowed).toBe(true);
|
|
149
|
-
expect(mockIsPermittedCached).toHaveBeenCalled();
|
|
150
|
-
});
|
|
112
|
+
// Note: Permission checking tests have been moved to useCan.test.ts
|
|
113
|
+
// useRBAC now focuses on role information and context, not permission checks
|
|
151
114
|
|
|
152
115
|
it('handles denied app resolution securely', async () => {
|
|
153
116
|
mockUseUnifiedAuth.mockReturnValue({
|
|
@@ -17,7 +17,6 @@ import { useEvents } from '../../hooks/useEvents';
|
|
|
17
17
|
import {
|
|
18
18
|
getPermissionMap,
|
|
19
19
|
getAccessLevel,
|
|
20
|
-
isPermittedCached,
|
|
21
20
|
resolveAppContext,
|
|
22
21
|
getRoleContext,
|
|
23
22
|
} from '../api';
|
|
@@ -27,7 +26,6 @@ import type {
|
|
|
27
26
|
GlobalRole,
|
|
28
27
|
OrganisationRole,
|
|
29
28
|
EventAppRole,
|
|
30
|
-
Operation,
|
|
31
29
|
Permission,
|
|
32
30
|
Scope,
|
|
33
31
|
PermissionMap,
|
|
@@ -125,58 +123,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
125
123
|
}
|
|
126
124
|
}, [appName, logger, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user]);
|
|
127
125
|
|
|
128
|
-
const hasPermission = useCallback(
|
|
129
|
-
async (operationOrPermission: Operation | string, targetPageId?: string): Promise<boolean> => {
|
|
130
|
-
if (!user) {
|
|
131
|
-
logger.warn('[useRBAC] Permission check attempted without authenticated user context');
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!currentScope || !currentScope.organisationId) {
|
|
136
|
-
logger.error('[useRBAC] Permission check denied due to missing organisation context', {
|
|
137
|
-
hasScope: !!currentScope,
|
|
138
|
-
scope: currentScope,
|
|
139
|
-
userId: user.id,
|
|
140
|
-
permission: operationOrPermission,
|
|
141
|
-
});
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (globalRole === 'super_admin' || permissionMap['*']) {
|
|
146
|
-
logger.info('[useRBAC] Super admin bypass granted', {
|
|
147
|
-
userId: user.id,
|
|
148
|
-
permission: operationOrPermission,
|
|
149
|
-
});
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let permission: Permission;
|
|
154
|
-
if (operationOrPermission.includes(':')) {
|
|
155
|
-
permission = operationOrPermission as Permission;
|
|
156
|
-
} else if (targetPageId || pageId) {
|
|
157
|
-
permission = `${operationOrPermission}:${targetPageId || pageId}` as Permission;
|
|
158
|
-
} else {
|
|
159
|
-
permission = operationOrPermission as Permission;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const cachedValue = permissionMap[permission];
|
|
163
|
-
if (cachedValue === true) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
if (cachedValue === false) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return isPermittedCached({
|
|
171
|
-
userId: user.id as UUID,
|
|
172
|
-
scope: currentScope,
|
|
173
|
-
permission,
|
|
174
|
-
pageId: targetPageId ?? pageId,
|
|
175
|
-
});
|
|
176
|
-
},
|
|
177
|
-
[currentScope, globalRole, logger, pageId, permissionMap, user],
|
|
178
|
-
);
|
|
179
|
-
|
|
180
126
|
const hasGlobalPermission = useCallback(
|
|
181
127
|
(permission: string): boolean => {
|
|
182
128
|
if (globalRole === 'super_admin' || permissionMap['*']) {
|
|
@@ -211,7 +157,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
211
157
|
globalRole,
|
|
212
158
|
organisationRole,
|
|
213
159
|
eventAppRole,
|
|
214
|
-
hasPermission,
|
|
215
160
|
hasGlobalPermission,
|
|
216
161
|
isSuperAdmin,
|
|
217
162
|
isOrgAdmin,
|
|
@@ -74,28 +74,30 @@ export function useResolvedScope({
|
|
|
74
74
|
eventId: undefined
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Only update if the scope has actually changed
|
|
86
|
-
if (stableScopeRef.current.organisationId !== newScope.organisationId ||
|
|
87
|
-
stableScopeRef.current.eventId !== newScope.eventId ||
|
|
88
|
-
stableScopeRef.current.appId !== newScope.appId) {
|
|
89
|
-
stableScopeRef.current = {
|
|
90
|
-
organisationId: newScope.organisationId,
|
|
91
|
-
appId: newScope.appId || '',
|
|
92
|
-
eventId: newScope.eventId
|
|
77
|
+
// Update stable scope ref in useEffect to avoid updates during render
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (resolvedScope && resolvedScope.organisationId) {
|
|
80
|
+
const newScope = {
|
|
81
|
+
organisationId: resolvedScope.organisationId,
|
|
82
|
+
appId: resolvedScope.appId,
|
|
83
|
+
eventId: resolvedScope.eventId
|
|
93
84
|
};
|
|
85
|
+
|
|
86
|
+
// Only update if the scope has actually changed
|
|
87
|
+
if (stableScopeRef.current.organisationId !== newScope.organisationId ||
|
|
88
|
+
stableScopeRef.current.eventId !== newScope.eventId ||
|
|
89
|
+
stableScopeRef.current.appId !== newScope.appId) {
|
|
90
|
+
stableScopeRef.current = {
|
|
91
|
+
organisationId: newScope.organisationId,
|
|
92
|
+
appId: newScope.appId || '',
|
|
93
|
+
eventId: newScope.eventId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} else if (!resolvedScope) {
|
|
97
|
+
// Reset to empty scope when no resolved scope
|
|
98
|
+
stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
|
|
94
99
|
}
|
|
95
|
-
}
|
|
96
|
-
// Reset to empty scope when no resolved scope
|
|
97
|
-
stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
|
|
98
|
-
}
|
|
100
|
+
}, [resolvedScope]);
|
|
99
101
|
|
|
100
102
|
const stableScope = stableScopeRef.current;
|
|
101
103
|
|
|
@@ -144,17 +146,10 @@ export function useResolvedScope({
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
//
|
|
148
|
-
console.log('[useResolvedScope] Attempting to resolve scope:', {
|
|
149
|
-
selectedOrganisationId,
|
|
150
|
-
selectedEventId,
|
|
151
|
-
appId: appId || 'NOT RESOLVED YET',
|
|
152
|
-
resolveStep: 'initial'
|
|
153
|
-
});
|
|
149
|
+
// Resolve scope based on available context
|
|
154
150
|
|
|
155
151
|
// If we have both organisation and event, use them directly
|
|
156
152
|
if (selectedOrganisationId && selectedEventId) {
|
|
157
|
-
console.log('[useResolvedScope] Resolving with both org and event');
|
|
158
153
|
if (!cancelled) {
|
|
159
154
|
setResolvedScope({
|
|
160
155
|
organisationId: selectedOrganisationId,
|
|
@@ -168,7 +163,6 @@ export function useResolvedScope({
|
|
|
168
163
|
|
|
169
164
|
// If we only have organisation, use it
|
|
170
165
|
if (selectedOrganisationId) {
|
|
171
|
-
console.log('[useResolvedScope] Resolving with organisation only');
|
|
172
166
|
if (!cancelled) {
|
|
173
167
|
setResolvedScope({
|
|
174
168
|
organisationId: selectedOrganisationId,
|
|
@@ -183,7 +177,6 @@ export function useResolvedScope({
|
|
|
183
177
|
// If we only have event, resolve organisation from event
|
|
184
178
|
if (selectedEventId && supabase) {
|
|
185
179
|
try {
|
|
186
|
-
console.log('[useResolvedScope] Resolving from event:', { selectedEventId, appId });
|
|
187
180
|
const eventScope = await createScopeFromEvent(supabase, selectedEventId, appId);
|
|
188
181
|
if (!eventScope) {
|
|
189
182
|
console.error('[useResolvedScope] Could not resolve organization from event context');
|
|
@@ -194,7 +187,6 @@ export function useResolvedScope({
|
|
|
194
187
|
}
|
|
195
188
|
return;
|
|
196
189
|
}
|
|
197
|
-
console.log('[useResolvedScope] Resolved from event:', eventScope);
|
|
198
190
|
// Preserve the resolved app ID
|
|
199
191
|
if (!cancelled) {
|
|
200
192
|
setResolvedScope({
|
|
@@ -84,13 +84,17 @@ describe('RBAC Permissions', () => {
|
|
|
84
84
|
|
|
85
85
|
it('rejects invalid Permission types', () => {
|
|
86
86
|
const invalidPermissions = [
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
':users',
|
|
91
|
-
'read
|
|
92
|
-
'
|
|
93
|
-
'
|
|
87
|
+
'READ:users', // Capital letters not allowed
|
|
88
|
+
'read:', // Missing resource
|
|
89
|
+
':users', // Missing operation
|
|
90
|
+
'read:users*', // Invalid wildcard placement
|
|
91
|
+
'read:*users', // Invalid wildcard placement
|
|
92
|
+
'invalid', // Not in format operation:resource
|
|
93
|
+
'read:users-', // Invalid character (hyphen) at end
|
|
94
|
+
'read:users_', // Invalid character (underscore) at end
|
|
95
|
+
'read:.users', // Cannot start with dot
|
|
96
|
+
'read:users.', // Cannot end with dot
|
|
97
|
+
'read:users..detail', // Cannot have consecutive dots
|
|
94
98
|
];
|
|
95
99
|
|
|
96
100
|
invalidPermissions.forEach(permission => {
|
|
@@ -23,7 +23,7 @@ describe('RBACSecurityValidator', () => {
|
|
|
23
23
|
'create:user-profiles',
|
|
24
24
|
'update:events',
|
|
25
25
|
'delete:organisations',
|
|
26
|
-
'
|
|
26
|
+
'update:base.events',
|
|
27
27
|
'read:user-profiles.details'
|
|
28
28
|
];
|
|
29
29
|
|
|
@@ -352,7 +352,7 @@ describe('RBACSecurityMiddleware', () => {
|
|
|
352
352
|
const input = {
|
|
353
353
|
userId: '123e4567-e89b-12d3-a456-426614174000' as UUID,
|
|
354
354
|
scope: { organisationId: '123e4567-e89b-12d3-a456-426614174001' as UUID },
|
|
355
|
-
permission: '
|
|
355
|
+
permission: 'update:everything' as Permission
|
|
356
356
|
};
|
|
357
357
|
|
|
358
358
|
const context = {
|