@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
package/src/rbac/README.md
CHANGED
|
@@ -44,25 +44,15 @@ The RBAC system includes comprehensive security measures:
|
|
|
44
44
|
```typescript
|
|
45
45
|
import { isPermitted } from '@jmruthers/pace-core/rbac';
|
|
46
46
|
|
|
47
|
-
// Basic usage
|
|
47
|
+
// Basic usage - organisationId is required
|
|
48
48
|
const hasPermission = await isPermitted({
|
|
49
49
|
userId: 'user-123',
|
|
50
|
-
scope: { organisationId: 'org-456' },
|
|
51
|
-
permission: '
|
|
50
|
+
scope: { organisationId: 'org-456' }, // organisationId is required
|
|
51
|
+
permission: 'update:events'
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
userId: 'user-123',
|
|
57
|
-
scope: { organisationId: 'org-456' },
|
|
58
|
-
permission: 'manage:events'
|
|
59
|
-
}, {
|
|
60
|
-
userId: 'user-123',
|
|
61
|
-
organisationId: 'org-456',
|
|
62
|
-
ipAddress: '192.168.1.1',
|
|
63
|
-
userAgent: 'Mozilla/5.0...',
|
|
64
|
-
timestamp: new Date()
|
|
65
|
-
});
|
|
54
|
+
// Security context is automatically created internally
|
|
55
|
+
// If organisationId is missing, OrganisationContextRequiredError is thrown
|
|
66
56
|
```
|
|
67
57
|
|
|
68
58
|
### Security Context Examples
|
|
@@ -77,17 +67,16 @@ app.get('/api/users', async (req, res) => {
|
|
|
77
67
|
const userId = req.user.id;
|
|
78
68
|
const organisationId = req.user.organisationId;
|
|
79
69
|
|
|
80
|
-
//
|
|
70
|
+
// organisationId is required - if missing, OrganisationContextRequiredError is thrown
|
|
71
|
+
if (!organisationId) {
|
|
72
|
+
return res.status(400).json({ error: 'Organisation context is required' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check permission - security context is created automatically
|
|
81
76
|
const hasAccess = await isPermitted({
|
|
82
77
|
userId,
|
|
83
|
-
scope: { organisationId },
|
|
78
|
+
scope: { organisationId }, // Required
|
|
84
79
|
permission: 'read:users'
|
|
85
|
-
}, {
|
|
86
|
-
userId,
|
|
87
|
-
organisationId,
|
|
88
|
-
ipAddress: req.ip,
|
|
89
|
-
userAgent: req.get('User-Agent'),
|
|
90
|
-
timestamp: new Date()
|
|
91
80
|
});
|
|
92
81
|
|
|
93
82
|
if (!hasAccess) {
|
|
@@ -111,7 +100,7 @@ function UserManagement() {
|
|
|
111
100
|
const { hasPermission, isLoading } = useCan(
|
|
112
101
|
user.id,
|
|
113
102
|
{ organisationId: user.organisationId },
|
|
114
|
-
'
|
|
103
|
+
'update:users',
|
|
115
104
|
true, // useCache
|
|
116
105
|
{
|
|
117
106
|
userId: user.id,
|
|
@@ -172,7 +161,7 @@ function AdminDashboard() {
|
|
|
172
161
|
[
|
|
173
162
|
'read:users',
|
|
174
163
|
'create:events',
|
|
175
|
-
'
|
|
164
|
+
'update:organisations',
|
|
176
165
|
'delete:data'
|
|
177
166
|
],
|
|
178
167
|
{
|
|
@@ -190,7 +179,7 @@ function AdminDashboard() {
|
|
|
190
179
|
<div>
|
|
191
180
|
{permissions['read:users'] && <UserManagement />}
|
|
192
181
|
{permissions['create:events'] && <EventCreation />}
|
|
193
|
-
{permissions['
|
|
182
|
+
{permissions['update:organisations'] && <OrganisationSettings />}
|
|
194
183
|
</div>
|
|
195
184
|
);
|
|
196
185
|
}
|
|
@@ -283,7 +272,7 @@ function UserActions() {
|
|
|
283
272
|
return (
|
|
284
273
|
<div>
|
|
285
274
|
{permissions['page-1']?.includes('read') && <ReadButton />}
|
|
286
|
-
{permissions['page-1']?.includes('
|
|
275
|
+
{permissions['page-1']?.includes('update') && <UpdateButton />}
|
|
287
276
|
</div>
|
|
288
277
|
);
|
|
289
278
|
}
|
|
@@ -308,7 +297,7 @@ function UserActions() {
|
|
|
308
297
|
eventId: undefined,
|
|
309
298
|
appId: undefined
|
|
310
299
|
},
|
|
311
|
-
'
|
|
300
|
+
'update:users',
|
|
312
301
|
'page-123' // optional pageId
|
|
313
302
|
);
|
|
314
303
|
|
|
@@ -447,7 +436,7 @@ import { isPermitted, getAccessLevel, getPermissionMap } from '@jmruthers/pace-c
|
|
|
447
436
|
const canManage = await isPermitted({
|
|
448
437
|
userId: 'user-123',
|
|
449
438
|
scope: { organisationId: 'org-456' },
|
|
450
|
-
permission: '
|
|
439
|
+
permission: 'update:events',
|
|
451
440
|
pageId: 'page-789'
|
|
452
441
|
});
|
|
453
442
|
|
|
@@ -482,7 +471,7 @@ function MyComponent() {
|
|
|
482
471
|
const { can, isLoading: checking } = useCan(
|
|
483
472
|
'user-123',
|
|
484
473
|
{ organisationId: 'org-456' },
|
|
485
|
-
'
|
|
474
|
+
'update:events'
|
|
486
475
|
);
|
|
487
476
|
|
|
488
477
|
const { accessLevel } = useAccessLevel(
|
|
@@ -511,7 +500,7 @@ function App() {
|
|
|
511
500
|
return (
|
|
512
501
|
<div>
|
|
513
502
|
<PermissionEnforcer
|
|
514
|
-
permissions={['
|
|
503
|
+
permissions={['update:events']}
|
|
515
504
|
operation="event-management"
|
|
516
505
|
fallback={<AccessDenied />}
|
|
517
506
|
>
|
|
@@ -537,7 +526,7 @@ import { withPermissionGuard, withAccessLevelGuard } from '@jmruthers/pace-core/
|
|
|
537
526
|
|
|
538
527
|
// Express middleware
|
|
539
528
|
app.get('/admin',
|
|
540
|
-
withPermissionGuard({ permission: '
|
|
529
|
+
withPermissionGuard({ permission: 'update:admin' },
|
|
541
530
|
(req, res) => {
|
|
542
531
|
res.json({ message: 'Admin access granted' });
|
|
543
532
|
}
|
|
@@ -566,7 +555,7 @@ Check if a user has a specific permission.
|
|
|
566
555
|
const hasPermission = await isPermitted({
|
|
567
556
|
userId: 'user-123',
|
|
568
557
|
scope: { organisationId: 'org-456' },
|
|
569
|
-
permission: '
|
|
558
|
+
permission: 'update:events',
|
|
570
559
|
pageId: 'page-789'
|
|
571
560
|
});
|
|
572
561
|
```
|
|
@@ -616,7 +605,7 @@ Hook to check if user has a specific permission.
|
|
|
616
605
|
const { can, isLoading, error, check } = useCan(
|
|
617
606
|
'user-123',
|
|
618
607
|
{ organisationId: 'org-456' },
|
|
619
|
-
'
|
|
608
|
+
'update:events',
|
|
620
609
|
'page-789'
|
|
621
610
|
);
|
|
622
611
|
```
|
|
@@ -639,14 +628,34 @@ const { accessLevel, isLoading, error, refetch } = useAccessLevel(
|
|
|
639
628
|
Conditionally renders children based on permissions. Works for both organization-based and event-based apps.
|
|
640
629
|
|
|
641
630
|
```tsx
|
|
631
|
+
// Single permission
|
|
642
632
|
<PermissionEnforcer
|
|
643
|
-
permissions={['
|
|
633
|
+
permissions={['update:events']}
|
|
644
634
|
operation="event-management"
|
|
645
635
|
fallback={<AccessDenied />}
|
|
646
|
-
onDenied={() => console.log('Access denied')}
|
|
647
636
|
>
|
|
648
637
|
<AdminPanel />
|
|
649
638
|
</PermissionEnforcer>
|
|
639
|
+
|
|
640
|
+
// Multiple permissions - require ALL (default)
|
|
641
|
+
<PermissionEnforcer
|
|
642
|
+
permissions={['read:events', 'update:events']}
|
|
643
|
+
operation="event-management"
|
|
644
|
+
requireAll={true}
|
|
645
|
+
fallback={<AccessDenied />}
|
|
646
|
+
>
|
|
647
|
+
<EventManagementPanel />
|
|
648
|
+
</PermissionEnforcer>
|
|
649
|
+
|
|
650
|
+
// Multiple permissions - require ANY
|
|
651
|
+
<PermissionEnforcer
|
|
652
|
+
permissions={['read:events', 'read:reports']}
|
|
653
|
+
operation="viewing"
|
|
654
|
+
requireAll={false}
|
|
655
|
+
fallback={<AccessDenied />}
|
|
656
|
+
>
|
|
657
|
+
<ViewingPanel />
|
|
658
|
+
</PermissionEnforcer>
|
|
650
659
|
```
|
|
651
660
|
|
|
652
661
|
#### `PagePermissionGuard`
|
|
@@ -676,6 +685,39 @@ Conditionally renders navigation items based on permissions.
|
|
|
676
685
|
</NavigationGuard>
|
|
677
686
|
```
|
|
678
687
|
|
|
688
|
+
#### `RoleBasedRouter`
|
|
689
|
+
|
|
690
|
+
Centralized routing with permission enforcement and public route support.
|
|
691
|
+
|
|
692
|
+
```tsx
|
|
693
|
+
const routes: RouteConfig[] = [
|
|
694
|
+
{
|
|
695
|
+
path: '/dashboard',
|
|
696
|
+
component: Dashboard,
|
|
697
|
+
permissions: ['read:dashboard'],
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
path: '/login',
|
|
701
|
+
component: Login,
|
|
702
|
+
public: true, // Public route - no permission checks
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
path: '/admin',
|
|
706
|
+
component: Admin,
|
|
707
|
+
permissions: ['update:admin'],
|
|
708
|
+
accessLevel: 'admin',
|
|
709
|
+
},
|
|
710
|
+
];
|
|
711
|
+
|
|
712
|
+
<RoleBasedRouter
|
|
713
|
+
routes={routes}
|
|
714
|
+
fallbackRoute="/unauthorized"
|
|
715
|
+
strictMode={true}
|
|
716
|
+
>
|
|
717
|
+
<App />
|
|
718
|
+
</RoleBasedRouter>
|
|
719
|
+
```
|
|
720
|
+
|
|
679
721
|
### Server Adapters
|
|
680
722
|
|
|
681
723
|
#### `withPermissionGuard(config, handler)`
|
|
@@ -684,7 +726,7 @@ Wraps a server handler with permission checking.
|
|
|
684
726
|
|
|
685
727
|
```typescript
|
|
686
728
|
const protectedHandler = withPermissionGuard(
|
|
687
|
-
{ permission: '
|
|
729
|
+
{ permission: 'update:events', pageId: 'page-789' },
|
|
688
730
|
async (req, res) => {
|
|
689
731
|
// Handler logic here
|
|
690
732
|
res.json({ success: true });
|
|
@@ -711,12 +753,12 @@ const adminHandler = withAccessLevelGuard(
|
|
|
711
753
|
### Core Types
|
|
712
754
|
|
|
713
755
|
```typescript
|
|
714
|
-
type Operation = 'read' | 'create' | 'update' | 'delete'
|
|
756
|
+
type Operation = 'read' | 'create' | 'update' | 'delete';
|
|
715
757
|
type Permission = `${Operation}:${string}`;
|
|
716
758
|
type AccessLevel = 'viewer' | 'participant' | 'planner' | 'admin' | 'super';
|
|
717
759
|
|
|
718
760
|
interface Scope {
|
|
719
|
-
organisationId
|
|
761
|
+
organisationId: UUID; // Required - can always be derived from event context in event-based apps
|
|
720
762
|
eventId?: string;
|
|
721
763
|
appId?: UUID;
|
|
722
764
|
}
|
|
@@ -686,7 +686,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
|
|
|
686
686
|
it('redirects to fallback URL when permission is denied', async () => {
|
|
687
687
|
const middleware = createRBACMiddleware({
|
|
688
688
|
protectedRoutes: [
|
|
689
|
-
{ path: '/admin', permission: '
|
|
689
|
+
{ path: '/admin', permission: 'update:admin' as Permission }
|
|
690
690
|
],
|
|
691
691
|
fallbackUrl: '/access-denied'
|
|
692
692
|
});
|
|
@@ -710,7 +710,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
|
|
|
710
710
|
it('calls next when permission is granted', async () => {
|
|
711
711
|
const middleware = createRBACMiddleware({
|
|
712
712
|
protectedRoutes: [
|
|
713
|
-
{ path: '/admin', permission: '
|
|
713
|
+
{ path: '/admin', permission: 'update:admin' as Permission }
|
|
714
714
|
],
|
|
715
715
|
fallbackUrl: '/access-denied'
|
|
716
716
|
});
|
|
@@ -734,7 +734,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
|
|
|
734
734
|
it('redirects to login when user context is missing', async () => {
|
|
735
735
|
const middleware = createRBACMiddleware({
|
|
736
736
|
protectedRoutes: [
|
|
737
|
-
{ path: '/admin', permission: '
|
|
737
|
+
{ path: '/admin', permission: 'update:admin' as Permission }
|
|
738
738
|
],
|
|
739
739
|
fallbackUrl: '/access-denied'
|
|
740
740
|
});
|
|
@@ -58,9 +58,10 @@ describe('RBACEngine - Comprehensive Tests', () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
// Helper function to create security context
|
|
61
|
-
|
|
61
|
+
// OrganisationId is required - use mockOrgId as default
|
|
62
|
+
const createSecurityContext = (userId: string, organisationId: string = mockOrgId) => ({
|
|
62
63
|
userId: userId as UUID,
|
|
63
|
-
organisationId: organisationId as UUID
|
|
64
|
+
organisationId: organisationId as UUID,
|
|
64
65
|
timestamp: new Date()
|
|
65
66
|
});
|
|
66
67
|
|
|
@@ -84,7 +85,7 @@ describe('RBACEngine - Comprehensive Tests', () => {
|
|
|
84
85
|
const permissionCheck: PermissionCheck = {
|
|
85
86
|
userId: '00000000-0000-0000-0000-000000000001' as UUID,
|
|
86
87
|
scope: { organisationId: '00000000-0000-0000-0000-000000000002' as UUID },
|
|
87
|
-
permission: '
|
|
88
|
+
permission: 'update:everything' as Permission
|
|
88
89
|
};
|
|
89
90
|
|
|
90
91
|
const securityContext = createSecurityContext('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002');
|
|
@@ -103,7 +104,7 @@ describe('RBACEngine - Comprehensive Tests', () => {
|
|
|
103
104
|
const permissionCheck: PermissionCheck = {
|
|
104
105
|
userId: 'regular-user-123' as UUID,
|
|
105
106
|
scope: { organisationId: '00000000-0000-0000-0000-000000000002' as UUID },
|
|
106
|
-
permission: '
|
|
107
|
+
permission: 'update:everything' as Permission
|
|
107
108
|
};
|
|
108
109
|
|
|
109
110
|
const securityContext = {
|
|
@@ -366,6 +367,8 @@ describe('RBACEngine - Comprehensive Tests', () => {
|
|
|
366
367
|
select: vi.fn().mockReturnThis(),
|
|
367
368
|
eq: vi.fn().mockReturnThis(),
|
|
368
369
|
is: vi.fn().mockReturnThis(),
|
|
370
|
+
lte: vi.fn().mockReturnThis(),
|
|
371
|
+
or: vi.fn().mockReturnThis(),
|
|
369
372
|
single: vi.fn().mockResolvedValue({
|
|
370
373
|
data: { role: 'org_admin' },
|
|
371
374
|
error: null
|
|
@@ -427,6 +430,8 @@ describe('RBACEngine - Comprehensive Tests', () => {
|
|
|
427
430
|
select: vi.fn().mockReturnThis(),
|
|
428
431
|
eq: vi.fn().mockReturnThis(),
|
|
429
432
|
is: vi.fn().mockReturnThis(),
|
|
433
|
+
lte: vi.fn().mockReturnThis(),
|
|
434
|
+
or: vi.fn().mockReturnThis(),
|
|
430
435
|
single: vi.fn().mockResolvedValue({
|
|
431
436
|
data: null,
|
|
432
437
|
error: { code: 'PGRST116' }
|
|
@@ -495,6 +500,8 @@ describe('RBACEngine - Comprehensive Tests', () => {
|
|
|
495
500
|
select: vi.fn().mockReturnThis(),
|
|
496
501
|
eq: vi.fn().mockReturnThis(),
|
|
497
502
|
is: vi.fn().mockReturnThis(),
|
|
503
|
+
lte: vi.fn().mockReturnThis(),
|
|
504
|
+
or: vi.fn().mockReturnThis(),
|
|
498
505
|
single: vi.fn().mockResolvedValue({
|
|
499
506
|
data: null,
|
|
500
507
|
error: { code: 'PGRST116' }
|
|
@@ -780,9 +787,11 @@ describe('RBACEngine - Comprehensive Tests', () => {
|
|
|
780
787
|
};
|
|
781
788
|
|
|
782
789
|
const securityContext = {
|
|
783
|
-
userId: 'user-123',
|
|
790
|
+
userId: 'user-123' as UUID,
|
|
791
|
+
organisationId: '00000000-0000-0000-0000-000000000002' as UUID, // Required
|
|
784
792
|
ipAddress: '192.168.1.1',
|
|
785
|
-
userAgent: 'test-agent'
|
|
793
|
+
userAgent: 'test-agent',
|
|
794
|
+
timestamp: new Date()
|
|
786
795
|
};
|
|
787
796
|
|
|
788
797
|
const result = await engine.isPermitted(permissionCheck, securityContext);
|
|
@@ -60,7 +60,7 @@ describe('RBACEngine - Core Logic Tests', () => {
|
|
|
60
60
|
const permissionCheck: PermissionCheck = {
|
|
61
61
|
userId: '00000000-0000-0000-0000-000000000001' as UUID,
|
|
62
62
|
scope: { organisationId: '00000000-0000-0000-0000-000000000002' as UUID },
|
|
63
|
-
permission: '
|
|
63
|
+
permission: 'update:everything' as Permission
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
const securityContext = {
|
|
@@ -131,7 +131,7 @@ describe('RBACEngine - Core Logic Tests', () => {
|
|
|
131
131
|
const permissionCheck: PermissionCheck = {
|
|
132
132
|
userId: 'user-123' as UUID,
|
|
133
133
|
scope: { organisationId: 'org-123' as UUID },
|
|
134
|
-
permission: '
|
|
134
|
+
permission: 'update:everything' as Permission
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
const result = await engine.isPermitted(permissionCheck);
|
|
@@ -187,6 +187,7 @@ describe('RBACEngine - Core Logic Tests', () => {
|
|
|
187
187
|
|
|
188
188
|
it('resolves organisation admin access level', async () => {
|
|
189
189
|
// Mock the from() chain to handle both global roles and organisation roles queries
|
|
190
|
+
let organisationRoleQuery: any;
|
|
190
191
|
mockSupabase.from.mockImplementation((table: string) => {
|
|
191
192
|
if (table === 'rbac_global_roles') {
|
|
192
193
|
return {
|
|
@@ -201,15 +202,20 @@ describe('RBACEngine - Core Logic Tests', () => {
|
|
|
201
202
|
};
|
|
202
203
|
}
|
|
203
204
|
if (table === 'rbac_organisation_roles') {
|
|
204
|
-
|
|
205
|
+
const query = {
|
|
205
206
|
select: vi.fn().mockReturnThis(),
|
|
206
207
|
eq: vi.fn().mockReturnThis(),
|
|
207
208
|
is: vi.fn().mockReturnThis(),
|
|
209
|
+
lte: vi.fn().mockReturnThis(),
|
|
210
|
+
or: vi.fn().mockReturnThis(),
|
|
208
211
|
single: vi.fn().mockResolvedValue({
|
|
209
212
|
data: { role: 'org_admin' },
|
|
210
213
|
error: null
|
|
211
214
|
})
|
|
212
215
|
};
|
|
216
|
+
|
|
217
|
+
organisationRoleQuery = query;
|
|
218
|
+
return query;
|
|
213
219
|
}
|
|
214
220
|
return {
|
|
215
221
|
select: vi.fn().mockReturnThis(),
|
|
@@ -222,8 +228,55 @@ describe('RBACEngine - Core Logic Tests', () => {
|
|
|
222
228
|
|
|
223
229
|
const scope: Scope = { organisationId: 'org-123' as UUID };
|
|
224
230
|
const accessLevel = await engine.getAccessLevel({ userId: 'user-123' as UUID, scope });
|
|
225
|
-
|
|
231
|
+
|
|
226
232
|
expect(accessLevel).toBe('admin');
|
|
233
|
+
expect(organisationRoleQuery.lte).toHaveBeenCalledWith('valid_from', expect.any(String));
|
|
234
|
+
expect(organisationRoleQuery.or).toHaveBeenCalledWith(expect.stringContaining('valid_to.gte.'));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('treats expired organisation roles as viewer access', async () => {
|
|
238
|
+
let organisationRoleQuery: any;
|
|
239
|
+
mockSupabase.from.mockImplementation((table: string) => {
|
|
240
|
+
if (table === 'rbac_global_roles') {
|
|
241
|
+
return {
|
|
242
|
+
select: vi.fn().mockReturnThis(),
|
|
243
|
+
eq: vi.fn().mockReturnThis(),
|
|
244
|
+
lte: vi.fn().mockReturnThis(),
|
|
245
|
+
or: vi.fn().mockReturnThis(),
|
|
246
|
+
limit: vi.fn().mockResolvedValue({
|
|
247
|
+
data: [],
|
|
248
|
+
error: null
|
|
249
|
+
})
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (table === 'rbac_organisation_roles') {
|
|
253
|
+
const query = {
|
|
254
|
+
select: vi.fn().mockReturnThis(),
|
|
255
|
+
eq: vi.fn().mockReturnThis(),
|
|
256
|
+
is: vi.fn().mockReturnThis(),
|
|
257
|
+
lte: vi.fn().mockReturnThis(),
|
|
258
|
+
or: vi.fn().mockReturnThis(),
|
|
259
|
+
single: vi.fn().mockResolvedValue({ data: null, error: null })
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
organisationRoleQuery = query;
|
|
263
|
+
return query;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
select: vi.fn().mockReturnThis(),
|
|
267
|
+
eq: vi.fn().mockReturnThis(),
|
|
268
|
+
lte: vi.fn().mockReturnThis(),
|
|
269
|
+
or: vi.fn().mockReturnThis(),
|
|
270
|
+
limit: vi.fn().mockResolvedValue({ data: [], error: null })
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const scope: Scope = { organisationId: 'org-123' as UUID };
|
|
275
|
+
const accessLevel = await engine.getAccessLevel({ userId: 'user-123' as UUID, scope });
|
|
276
|
+
|
|
277
|
+
expect(accessLevel).toBe('viewer');
|
|
278
|
+
expect(organisationRoleQuery.lte).toHaveBeenCalledWith('valid_from', expect.any(String));
|
|
279
|
+
expect(organisationRoleQuery.or).toHaveBeenCalledWith(expect.stringContaining('valid_to.gte.'));
|
|
227
280
|
});
|
|
228
281
|
});
|
|
229
282
|
|
|
@@ -71,8 +71,7 @@ describe('RBACEngine - Simplified Tests', () => {
|
|
|
71
71
|
const invalidPermissions = [
|
|
72
72
|
'invalid',
|
|
73
73
|
'read',
|
|
74
|
-
|
|
75
|
-
'manage:*',
|
|
74
|
+
// Note: manage operations have been removed
|
|
76
75
|
':users',
|
|
77
76
|
'read:user-settings', // hyphens not allowed
|
|
78
77
|
'read:user_settings', // underscores not allowed
|
|
@@ -175,6 +174,8 @@ describe('RBACEngine - Simplified Tests', () => {
|
|
|
175
174
|
select: vi.fn().mockReturnThis(),
|
|
176
175
|
eq: vi.fn().mockReturnThis(),
|
|
177
176
|
is: vi.fn().mockReturnThis(),
|
|
177
|
+
lte: vi.fn().mockReturnThis(),
|
|
178
|
+
or: vi.fn().mockReturnThis(),
|
|
178
179
|
single: vi.fn().mockResolvedValue({
|
|
179
180
|
data: { role: 'org_admin' },
|
|
180
181
|
error: null
|
package/src/rbac/adapters.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
|
|
|
30
30
|
* <PermissionGuard
|
|
31
31
|
* userId="user-123"
|
|
32
32
|
* scope={{ organisationId: 'org-456' }}
|
|
33
|
-
* permission="
|
|
33
|
+
* permission="update:events"
|
|
34
34
|
* pageId="page-789"
|
|
35
35
|
* fallback={<AccessDenied />}
|
|
36
36
|
* >
|
|
@@ -39,7 +39,7 @@ import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
|
|
|
39
39
|
*
|
|
40
40
|
* // With context inference (requires auth context)
|
|
41
41
|
* <PermissionGuard
|
|
42
|
-
* permission="
|
|
42
|
+
* permission="update:events"
|
|
43
43
|
* scope={{ organisationId: 'org-456' }}
|
|
44
44
|
* fallback={<AccessDenied />}
|
|
45
45
|
* >
|
|
@@ -279,7 +279,7 @@ export function AccessLevelGuard({
|
|
|
279
279
|
* @example
|
|
280
280
|
* ```typescript
|
|
281
281
|
* const protectedHandler = withPermissionGuard(
|
|
282
|
-
* { permission: '
|
|
282
|
+
* { permission: 'update:events', pageId: 'page-789' },
|
|
283
283
|
* async (req, res) => {
|
|
284
284
|
* // Handler logic here
|
|
285
285
|
* res.json({ success: true });
|
|
@@ -521,7 +521,7 @@ export function withRoleGuard<T extends any[]>(
|
|
|
521
521
|
*
|
|
522
522
|
* export default createRBACMiddleware({
|
|
523
523
|
* protectedRoutes: [
|
|
524
|
-
* { path: '/admin', permission: '
|
|
524
|
+
* { path: '/admin', permission: 'update:admin' },
|
|
525
525
|
* { path: '/events', permission: 'read:events' },
|
|
526
526
|
* ],
|
|
527
527
|
* fallbackUrl: '/access-denied',
|
package/src/rbac/api.test.ts
CHANGED
|
@@ -37,11 +37,11 @@ vi.mock('./cache', () => ({
|
|
|
37
37
|
)
|
|
38
38
|
},
|
|
39
39
|
CACHE_PATTERNS: {
|
|
40
|
-
PERMISSION: vi.fn((userId, organisationId) => `
|
|
41
|
-
USER: vi.fn((userId) =>
|
|
42
|
-
ORGANISATION: vi.fn((organisationId) =>
|
|
43
|
-
EVENT: vi.fn((eventId) =>
|
|
44
|
-
APP: vi.fn((appId) =>
|
|
40
|
+
PERMISSION: vi.fn((userId, organisationId) => `perm:${userId}:${organisationId}:`),
|
|
41
|
+
USER: vi.fn((userId) => `:${userId}:`),
|
|
42
|
+
ORGANISATION: vi.fn((organisationId) => `:${organisationId}:`),
|
|
43
|
+
EVENT: vi.fn((eventId) => `:${eventId}:`),
|
|
44
|
+
APP: vi.fn((appId) => `:${appId}`)
|
|
45
45
|
}
|
|
46
46
|
}));
|
|
47
47
|
|
|
@@ -119,7 +119,7 @@ describe('RBAC API', () => {
|
|
|
119
119
|
|
|
120
120
|
process.env.NODE_ENV = originalEnv;
|
|
121
121
|
|
|
122
|
-
expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
|
|
122
|
+
expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
|
|
123
123
|
expect(mockCreateAuditManager).toHaveBeenCalledWith(mockSupabase);
|
|
124
124
|
expect(mockSetGlobalAuditManager).toHaveBeenCalledWith(mockAuditManager);
|
|
125
125
|
expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
|
|
@@ -203,7 +203,7 @@ describe('RBAC API', () => {
|
|
|
203
203
|
|
|
204
204
|
setupRBAC(mockSupabase as any);
|
|
205
205
|
|
|
206
|
-
expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
|
|
206
|
+
expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
it('handles multiple initialization calls', () => {
|
|
@@ -622,6 +622,26 @@ describe('RBAC API', () => {
|
|
|
622
622
|
permission: 'read:users'
|
|
623
623
|
})).rejects.toThrow('Engine error');
|
|
624
624
|
});
|
|
625
|
+
|
|
626
|
+
it('throws OrganisationContextRequiredError when organisationId is missing', async () => {
|
|
627
|
+
const { isPermitted, OrganisationContextRequiredError } = await import('./api');
|
|
628
|
+
|
|
629
|
+
await expect(isPermitted({
|
|
630
|
+
userId: 'user-123',
|
|
631
|
+
scope: {}, // Missing organisationId
|
|
632
|
+
permission: 'read:users'
|
|
633
|
+
})).rejects.toThrow(OrganisationContextRequiredError);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('throws OrganisationContextRequiredError when organisationId is undefined', async () => {
|
|
637
|
+
const { isPermitted, OrganisationContextRequiredError } = await import('./api');
|
|
638
|
+
|
|
639
|
+
await expect(isPermitted({
|
|
640
|
+
userId: 'user-123',
|
|
641
|
+
scope: { organisationId: undefined },
|
|
642
|
+
permission: 'read:users'
|
|
643
|
+
})).rejects.toThrow(OrganisationContextRequiredError);
|
|
644
|
+
});
|
|
625
645
|
});
|
|
626
646
|
|
|
627
647
|
describe('isPermittedCached', () => {
|
|
@@ -919,28 +939,32 @@ describe('RBAC API', () => {
|
|
|
919
939
|
debug: vi.fn()
|
|
920
940
|
});
|
|
921
941
|
|
|
942
|
+
vi.mocked(rbacCache.invalidate).mockClear();
|
|
943
|
+
|
|
922
944
|
setupRBAC(mockSupabase as any);
|
|
923
945
|
});
|
|
924
946
|
|
|
925
947
|
describe('invalidateUserCache', () => {
|
|
926
948
|
it('invalidates user cache with organisation', async () => {
|
|
927
949
|
const { invalidateUserCache } = await import('./api');
|
|
928
|
-
|
|
950
|
+
|
|
929
951
|
invalidateUserCache('user-123', 'org-456');
|
|
930
952
|
|
|
931
|
-
expect(rbacCache.invalidate).toHaveBeenCalledWith(
|
|
932
|
-
|
|
933
|
-
);
|
|
953
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:user-123:org-456:');
|
|
954
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:user-123:org-456:');
|
|
955
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:user-123:org-456:');
|
|
956
|
+
expect(rbacCache.invalidate).toHaveBeenCalledTimes(3);
|
|
934
957
|
});
|
|
935
958
|
|
|
936
959
|
it('invalidates user cache without organisation', async () => {
|
|
937
960
|
const { invalidateUserCache } = await import('./api');
|
|
938
|
-
|
|
961
|
+
|
|
939
962
|
invalidateUserCache('user-123');
|
|
940
963
|
|
|
941
|
-
expect(rbacCache.invalidate).toHaveBeenCalledWith(
|
|
942
|
-
|
|
943
|
-
);
|
|
964
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:user-123:');
|
|
965
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:user-123:');
|
|
966
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:user-123:');
|
|
967
|
+
expect(rbacCache.invalidate).toHaveBeenCalledTimes(3);
|
|
944
968
|
});
|
|
945
969
|
});
|
|
946
970
|
|