@jmruthers/pace-core 0.6.4 → 0.6.5
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-E7YQZD7D.js → DataTable-AOVNCPTX.js} +8 -8
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-QTFVrL-Z.d.ts} +65 -83
- package/dist/{UnifiedAuthProvider-QPXO24B4.js → UnifiedAuthProvider-4SBX4LU5.js} +4 -4
- package/dist/{api-6LVZTHDS.js → api-O6HTBX5Y.js} +3 -3
- package/dist/{chunk-I6DAQMWX.js → chunk-6COVEUS7.js} +130 -106
- package/dist/chunk-6COVEUS7.js.map +1 -0
- package/dist/{chunk-36LVWXB2.js → chunk-AFVQODI2.js} +37 -1
- package/dist/{chunk-36LVWXB2.js.map → chunk-AFVQODI2.js.map} +1 -1
- package/dist/{chunk-3LPHPB62.js → chunk-EFN2EIMK.js} +2 -2
- package/dist/{chunk-ATKZM7RX.js → chunk-G7QEZTYQ.js} +31 -31
- package/dist/{chunk-ATKZM7RX.js.map → chunk-G7QEZTYQ.js.map} +1 -1
- package/dist/{chunk-NN6WWZ5U.js → chunk-HU2C6SSC.js} +29 -18
- package/dist/chunk-HU2C6SSC.js.map +1 -0
- package/dist/{chunk-AVMLPIM7.js → chunk-IHB5DR3H.js} +102 -51
- package/dist/chunk-IHB5DR3H.js.map +1 -0
- package/dist/{chunk-7JPAB3T5.js → chunk-IVOFDYWT.js} +364 -208
- package/dist/chunk-IVOFDYWT.js.map +1 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-JGRYX5UX.js} +120 -20
- package/dist/chunk-JGRYX5UX.js.map +1 -0
- package/dist/{chunk-OEWDTMG7.js → chunk-NTM7ZSB6.js} +4 -4
- package/dist/chunk-NTM7ZSB6.js.map +1 -0
- package/dist/{chunk-5EC5MEWX.js → chunk-RGAWHO7N.js} +4 -4
- package/dist/chunk-RGAWHO7N.js.map +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-UPPMRMYG.js} +3 -3
- package/dist/{chunk-YKRAFF5K.js.map → chunk-UPPMRMYG.js.map} +1 -1
- package/dist/components.d.ts +2 -3
- package/dist/components.js +24 -28
- package/dist/components.js.map +1 -1
- package/dist/{contextValidator-OOPCLPZW.js → contextValidator-5OGXSPKS.js} +2 -2
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +41 -139
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +27 -18
- package/dist/index.js +41 -50
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -3
- package/dist/rbac/index.d.ts +16 -9
- package/dist/rbac/index.js +6 -6
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-ClnV4tnv.d.ts} +8 -8
- package/dist/utils.js +1 -1
- package/docs/api/modules.md +210 -100
- package/package.json +1 -2
- package/scripts/validate-master.js +1 -1
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.tsx +17 -19
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +248 -228
- package/src/components/Dialog/Dialog.tsx +455 -325
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +41 -0
- package/src/components/FileDisplay/FileDisplay.tsx +5 -5
- package/src/components/Form/Form.test.tsx +3 -2
- package/src/components/Form/Form.tsx +4 -5
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +32 -39
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +36 -34
- package/src/components/index.ts +3 -4
- package/src/hooks/useEventTheme.ts +4 -4
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +4 -4
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/index.ts +11 -1
- package/src/rbac/README.md +20 -20
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResourcePermissions.test.ts +125 -30
- package/src/rbac/hooks/useResourcePermissions.ts +57 -29
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +36 -0
- package/src/services/AuthService.ts +2 -5
- package/src/services/InactivityService.ts +139 -58
- package/src/styles/core.css +4 -0
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- /package/dist/{DataTable-E7YQZD7D.js.map → DataTable-AOVNCPTX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-QPXO24B4.js.map → UnifiedAuthProvider-4SBX4LU5.js.map} +0 -0
- /package/dist/{api-6LVZTHDS.js.map → api-O6HTBX5Y.js.map} +0 -0
- /package/dist/{chunk-3LPHPB62.js.map → chunk-EFN2EIMK.js.map} +0 -0
- /package/dist/{contextValidator-OOPCLPZW.js.map → contextValidator-5OGXSPKS.js.map} +0 -0
package/src/rbac/README.md
CHANGED
|
@@ -229,18 +229,18 @@ import { PermissionEnforcer } from '@jmruthers/pace-core/rbac';
|
|
|
229
229
|
|
|
230
230
|
function Dashboard() {
|
|
231
231
|
return (
|
|
232
|
-
<
|
|
232
|
+
<main>
|
|
233
233
|
<h1>Dashboard</h1>
|
|
234
234
|
|
|
235
235
|
{/* Automatic scope resolution - no manual context needed */}
|
|
236
236
|
<PermissionEnforcer
|
|
237
237
|
permissions={['read:admin']}
|
|
238
238
|
operation="admin-panel"
|
|
239
|
-
fallback={<
|
|
239
|
+
fallback={<main>Access Denied</main>}
|
|
240
240
|
>
|
|
241
241
|
<AdminPanel />
|
|
242
242
|
</PermissionEnforcer>
|
|
243
|
-
</
|
|
243
|
+
</main>
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
246
|
```
|
|
@@ -266,14 +266,14 @@ function UserActions() {
|
|
|
266
266
|
}
|
|
267
267
|
);
|
|
268
268
|
|
|
269
|
-
if (isLoading) return <
|
|
270
|
-
if (error) return <
|
|
269
|
+
if (isLoading) return <main>Loading permissions...</main>;
|
|
270
|
+
if (error) return <main>Error: {error.message}</main>;
|
|
271
271
|
|
|
272
272
|
return (
|
|
273
|
-
<
|
|
273
|
+
<section>
|
|
274
274
|
{permissions['page-1']?.includes('read') && <ReadButton />}
|
|
275
275
|
{permissions['page-1']?.includes('update') && <UpdateButton />}
|
|
276
|
-
</
|
|
276
|
+
</section>
|
|
277
277
|
);
|
|
278
278
|
}
|
|
279
279
|
```
|
|
@@ -301,13 +301,13 @@ function UserActions() {
|
|
|
301
301
|
'page-123' // optional pageId
|
|
302
302
|
);
|
|
303
303
|
|
|
304
|
-
if (isLoading) return <
|
|
305
|
-
if (error) return <
|
|
304
|
+
if (isLoading) return <main>Checking permission...</main>;
|
|
305
|
+
if (error) return <main>Error: {error.message}</main>;
|
|
306
306
|
|
|
307
307
|
return (
|
|
308
|
-
<
|
|
308
|
+
<section>
|
|
309
309
|
{can ? <AdminPanel /> : <AccessDenied />}
|
|
310
|
-
</
|
|
310
|
+
</section>
|
|
311
311
|
);
|
|
312
312
|
}
|
|
313
313
|
```
|
|
@@ -321,7 +321,7 @@ import { PermissionEnforcer, PagePermissionGuard } from '@jmruthers/pace-core/rb
|
|
|
321
321
|
|
|
322
322
|
function EventDashboard() {
|
|
323
323
|
return (
|
|
324
|
-
<
|
|
324
|
+
<main>
|
|
325
325
|
<h1>Event Dashboard</h1>
|
|
326
326
|
|
|
327
327
|
{/* Organization is automatically resolved from event context */}
|
|
@@ -338,7 +338,7 @@ function EventDashboard() {
|
|
|
338
338
|
>
|
|
339
339
|
<EventSettings />
|
|
340
340
|
</PagePermissionGuard>
|
|
341
|
-
</
|
|
341
|
+
</main>
|
|
342
342
|
);
|
|
343
343
|
}
|
|
344
344
|
```
|
|
@@ -374,7 +374,7 @@ function EventDashboard() {
|
|
|
374
374
|
```tsx
|
|
375
375
|
const { selectedOrganisationId } = useUnifiedAuth();
|
|
376
376
|
if (!selectedOrganisationId) {
|
|
377
|
-
return <
|
|
377
|
+
return <main>Please select an organisation first</main>;
|
|
378
378
|
}
|
|
379
379
|
```
|
|
380
380
|
|
|
@@ -479,14 +479,14 @@ function MyComponent() {
|
|
|
479
479
|
{ organisationId: 'org-456' }
|
|
480
480
|
);
|
|
481
481
|
|
|
482
|
-
if (isLoading) return <
|
|
483
|
-
if (error) return <
|
|
482
|
+
if (isLoading) return <main>Loading...</main>;
|
|
483
|
+
if (error) return <main>Error: {error.message}</main>;
|
|
484
484
|
|
|
485
485
|
return (
|
|
486
|
-
<
|
|
486
|
+
<section>
|
|
487
487
|
{can && <AdminPanel />}
|
|
488
488
|
{accessLevel === 'admin' && <AdminControls />}
|
|
489
|
-
</
|
|
489
|
+
</section>
|
|
490
490
|
);
|
|
491
491
|
}
|
|
492
492
|
```
|
|
@@ -498,7 +498,7 @@ import { PermissionEnforcer, PagePermissionGuard } from '@jmruthers/pace-core/rb
|
|
|
498
498
|
|
|
499
499
|
function App() {
|
|
500
500
|
return (
|
|
501
|
-
<
|
|
501
|
+
<main>
|
|
502
502
|
<PermissionEnforcer
|
|
503
503
|
permissions={['update:events']}
|
|
504
504
|
operation="event-management"
|
|
@@ -514,7 +514,7 @@ function App() {
|
|
|
514
514
|
>
|
|
515
515
|
<AdminPanel />
|
|
516
516
|
</PagePermissionGuard>
|
|
517
|
-
</
|
|
517
|
+
</main>
|
|
518
518
|
);
|
|
519
519
|
}
|
|
520
520
|
```
|
|
@@ -83,12 +83,15 @@ describe('useRBAC', () => {
|
|
|
83
83
|
user: { id: 'user-1' },
|
|
84
84
|
session: { access_token: 'token' },
|
|
85
85
|
appName: 'test-app',
|
|
86
|
+
appId: undefined, // Ensure appId is undefined so resolveAppContext is called
|
|
87
|
+
contextAppId: undefined, // Also ensure contextAppId is undefined
|
|
86
88
|
appConfig: { requires_event: false },
|
|
87
89
|
selectedOrganisation: { id: 'org-1' },
|
|
88
90
|
isContextReady: true,
|
|
89
91
|
organisationLoading: false,
|
|
90
92
|
selectedEvent: null,
|
|
91
93
|
eventLoading: false,
|
|
94
|
+
supabase: {} as any,
|
|
92
95
|
});
|
|
93
96
|
mockUseOrganisations.mockReturnValue({ selectedOrganisation: { id: 'org-1' } });
|
|
94
97
|
|
|
@@ -98,15 +101,29 @@ describe('useRBAC', () => {
|
|
|
98
101
|
organisationRole: null,
|
|
99
102
|
eventAppRole: null,
|
|
100
103
|
});
|
|
104
|
+
// Ensure resolveAppContext returns a value so the hook continues
|
|
105
|
+
mockResolveAppContext.mockResolvedValue({ appId: 'app-123' as any, hasAccess: true });
|
|
101
106
|
|
|
102
107
|
const { result } = renderHook(() => useRBAC('dashboard'));
|
|
103
108
|
|
|
104
109
|
await waitFor(() => {
|
|
105
110
|
expect(result.current.isLoading).toBe(false);
|
|
106
111
|
expect(result.current.hasGlobalPermission).toBeTypeOf('function');
|
|
112
|
+
}, { timeout: 5000 });
|
|
113
|
+
|
|
114
|
+
// resolveAppContext should be called when appName is set and appId/contextAppId are undefined
|
|
115
|
+
// The function signature is: resolveAppContext({ userId, appName })
|
|
116
|
+
// Note: The error message format may be confusing - check actual call structure
|
|
117
|
+
expect(mockResolveAppContext).toHaveBeenCalled();
|
|
118
|
+
// Verify it was called with correct structure (userId and appName in first argument)
|
|
119
|
+
const calls = mockResolveAppContext.mock.calls;
|
|
120
|
+
const hasCorrectCall = calls.some(call => {
|
|
121
|
+
const firstArg = call[0];
|
|
122
|
+
return firstArg && typeof firstArg === 'object' &&
|
|
123
|
+
'userId' in firstArg && firstArg.userId === 'user-1' &&
|
|
124
|
+
'appName' in firstArg && firstArg.appName === 'test-app';
|
|
107
125
|
});
|
|
108
|
-
|
|
109
|
-
expect(mockResolveAppContext).toHaveBeenCalledWith({ userId: 'user-1', appName: 'test-app' });
|
|
126
|
+
expect(hasCorrectCall).toBe(true);
|
|
110
127
|
expect(mockGetPermissionMap).toHaveBeenCalledWith(
|
|
111
128
|
{
|
|
112
129
|
userId: 'user-1',
|
|
@@ -115,7 +132,8 @@ describe('useRBAC', () => {
|
|
|
115
132
|
eventId: undefined,
|
|
116
133
|
appId: 'app-123'
|
|
117
134
|
}
|
|
118
|
-
}
|
|
135
|
+
},
|
|
136
|
+
'test-app' // appName is passed as second argument to getPermissionMap
|
|
119
137
|
);
|
|
120
138
|
});
|
|
121
139
|
|
|
@@ -196,10 +196,11 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
196
196
|
setCurrentScope(resolvedScope);
|
|
197
197
|
|
|
198
198
|
// API calls no longer need appConfig (scope is page-level)
|
|
199
|
+
// Pass appName to allow PORTAL/ADMIN apps to work without organisation context
|
|
199
200
|
const [map, roleContext, accessLevel] = await Promise.all([
|
|
200
|
-
getPermissionMap({ userId: user.id as UUID, scope: resolvedScope }),
|
|
201
|
-
getRoleContext({ userId: user.id as UUID, scope: resolvedScope }),
|
|
202
|
-
getAccessLevel({ userId: user.id as UUID, scope: resolvedScope }),
|
|
201
|
+
getPermissionMap({ userId: user.id as UUID, scope: resolvedScope }, appName),
|
|
202
|
+
getRoleContext({ userId: user.id as UUID, scope: resolvedScope }, appName),
|
|
203
|
+
getAccessLevel({ userId: user.id as UUID, scope: resolvedScope }, appName),
|
|
203
204
|
]);
|
|
204
205
|
|
|
205
206
|
setPermissionMap(map);
|
|
@@ -137,11 +137,12 @@ describe('useResourcePermissions Hook', () => {
|
|
|
137
137
|
renderHook(() => useResourcePermissions('contacts'));
|
|
138
138
|
|
|
139
139
|
expect(mockUseCan).toHaveBeenCalledTimes(4); // create, update, delete, read
|
|
140
|
-
// When appId is available in scope,
|
|
140
|
+
// When appId is available in scope, permission strings include page. prefix
|
|
141
|
+
// and resource name is passed as pageId to enable page permission checks
|
|
141
142
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
142
143
|
'user-123',
|
|
143
144
|
mockScope,
|
|
144
|
-
'create:contacts',
|
|
145
|
+
'create:page.contacts',
|
|
145
146
|
'contacts', // pageId is resource name when appId is available
|
|
146
147
|
true,
|
|
147
148
|
null, // precomputedSuperAdmin
|
|
@@ -150,7 +151,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
150
151
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
151
152
|
'user-123',
|
|
152
153
|
mockScope,
|
|
153
|
-
'update:contacts',
|
|
154
|
+
'update:page.contacts',
|
|
154
155
|
'contacts', // pageId is resource name when appId is available
|
|
155
156
|
true,
|
|
156
157
|
null, // precomputedSuperAdmin
|
|
@@ -159,7 +160,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
159
160
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
160
161
|
'user-123',
|
|
161
162
|
mockScope,
|
|
162
|
-
'delete:contacts',
|
|
163
|
+
'delete:page.contacts',
|
|
163
164
|
'contacts', // pageId is resource name when appId is available
|
|
164
165
|
true,
|
|
165
166
|
null, // precomputedSuperAdmin
|
|
@@ -168,7 +169,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
168
169
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
169
170
|
'user-123',
|
|
170
171
|
mockScope,
|
|
171
|
-
'read:contacts',
|
|
172
|
+
'read:page.contacts',
|
|
172
173
|
'contacts', // pageId is resource name when appId is available
|
|
173
174
|
true,
|
|
174
175
|
null, // precomputedSuperAdmin
|
|
@@ -193,7 +194,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
193
194
|
|
|
194
195
|
it('returns false when user cannot create resource', () => {
|
|
195
196
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
196
|
-
if (permission === 'create:contacts') {
|
|
197
|
+
if (permission === 'create:page.contacts') {
|
|
197
198
|
return {
|
|
198
199
|
can: false,
|
|
199
200
|
isLoading: false,
|
|
@@ -234,7 +235,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
234
235
|
|
|
235
236
|
it('checks read permissions when enableRead is true', () => {
|
|
236
237
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
237
|
-
if (permission === 'read:contacts') {
|
|
238
|
+
if (permission === 'read:page.contacts') {
|
|
238
239
|
return {
|
|
239
240
|
can: true,
|
|
240
241
|
isLoading: false,
|
|
@@ -259,7 +260,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
259
260
|
|
|
260
261
|
it('returns false for read when permission is denied and enableRead is true', () => {
|
|
261
262
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
262
|
-
if (permission === 'read:contacts') {
|
|
263
|
+
if (permission === 'read:page.contacts') {
|
|
263
264
|
return {
|
|
264
265
|
can: false,
|
|
265
266
|
isLoading: false,
|
|
@@ -407,11 +408,11 @@ describe('useResourcePermissions Hook', () => {
|
|
|
407
408
|
const { result } = renderHook(() => useResourcePermissions('contacts'));
|
|
408
409
|
|
|
409
410
|
// When user is null, userId is empty string, but scope and permissions are still checked
|
|
410
|
-
// Since mockScope has appId, pageId will be the resource name ('contacts')
|
|
411
|
+
// Since mockScope has appId, permission strings include page. prefix and pageId will be the resource name ('contacts')
|
|
411
412
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
412
413
|
'',
|
|
413
414
|
mockScope,
|
|
414
|
-
'create:contacts',
|
|
415
|
+
'create:page.contacts',
|
|
415
416
|
'contacts', // pageId is resource name when appId is available in scope
|
|
416
417
|
true,
|
|
417
418
|
null, // precomputedSuperAdmin
|
|
@@ -420,7 +421,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
420
421
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
421
422
|
'',
|
|
422
423
|
mockScope,
|
|
423
|
-
'update:contacts',
|
|
424
|
+
'update:page.contacts',
|
|
424
425
|
'contacts',
|
|
425
426
|
true,
|
|
426
427
|
null, // precomputedSuperAdmin
|
|
@@ -429,7 +430,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
429
430
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
430
431
|
'',
|
|
431
432
|
mockScope,
|
|
432
|
-
'delete:contacts',
|
|
433
|
+
'delete:page.contacts',
|
|
433
434
|
'contacts',
|
|
434
435
|
true,
|
|
435
436
|
null, // precomputedSuperAdmin
|
|
@@ -438,7 +439,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
438
439
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
439
440
|
'',
|
|
440
441
|
mockScope,
|
|
441
|
-
'read:contacts',
|
|
442
|
+
'read:page.contacts',
|
|
442
443
|
'contacts',
|
|
443
444
|
true,
|
|
444
445
|
null, // precomputedSuperAdmin
|
|
@@ -481,7 +482,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
481
482
|
|
|
482
483
|
it('aggregates loading states from permission checks', () => {
|
|
483
484
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
484
|
-
if (permission === 'create:contacts') {
|
|
485
|
+
if (permission === 'create:page.contacts') {
|
|
485
486
|
return {
|
|
486
487
|
can: false,
|
|
487
488
|
isLoading: true,
|
|
@@ -504,7 +505,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
504
505
|
|
|
505
506
|
it('includes read loading state when enableRead is true', () => {
|
|
506
507
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
507
|
-
if (permission === 'read:contacts') {
|
|
508
|
+
if (permission === 'read:page.contacts') {
|
|
508
509
|
return {
|
|
509
510
|
can: false,
|
|
510
511
|
isLoading: true,
|
|
@@ -529,7 +530,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
529
530
|
|
|
530
531
|
it('excludes read loading state when enableRead is false', () => {
|
|
531
532
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
532
|
-
if (permission === 'read:contacts') {
|
|
533
|
+
if (permission === 'read:page.contacts') {
|
|
533
534
|
return {
|
|
534
535
|
can: false,
|
|
535
536
|
isLoading: true,
|
|
@@ -568,7 +569,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
568
569
|
it('aggregates errors from permission checks', () => {
|
|
569
570
|
const permissionError = new Error('Permission check failed');
|
|
570
571
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
571
|
-
if (permission === 'create:contacts') {
|
|
572
|
+
if (permission === 'create:page.contacts') {
|
|
572
573
|
return {
|
|
573
574
|
can: false,
|
|
574
575
|
isLoading: false,
|
|
@@ -600,7 +601,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
600
601
|
});
|
|
601
602
|
|
|
602
603
|
mockUseCan.mockImplementation((userId, scope, permission) => {
|
|
603
|
-
if (permission === 'create:contacts') {
|
|
604
|
+
if (permission === 'create:page.contacts') {
|
|
604
605
|
return {
|
|
605
606
|
can: false,
|
|
606
607
|
isLoading: false,
|
|
@@ -632,11 +633,11 @@ describe('useResourcePermissions Hook', () => {
|
|
|
632
633
|
it('respects enableRead option', () => {
|
|
633
634
|
renderHook(() => useResourcePermissions('contacts', { enableRead: true }));
|
|
634
635
|
|
|
635
|
-
// Should still call useCan for read permission
|
|
636
|
+
// Should still call useCan for read permission with page. prefix when appId is available
|
|
636
637
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
637
638
|
expect.any(String),
|
|
638
639
|
expect.any(Object),
|
|
639
|
-
'read:contacts',
|
|
640
|
+
'read:page.contacts',
|
|
640
641
|
'contacts', // pageId is resource name when appId is available
|
|
641
642
|
true,
|
|
642
643
|
null, // precomputedSuperAdmin
|
|
@@ -659,7 +660,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
659
660
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
660
661
|
expect.any(String),
|
|
661
662
|
expect.any(Object),
|
|
662
|
-
'create:risks',
|
|
663
|
+
'create:page.risks',
|
|
663
664
|
'risks', // pageId is resource name when appId is available
|
|
664
665
|
true,
|
|
665
666
|
null, // precomputedSuperAdmin
|
|
@@ -668,7 +669,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
668
669
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
669
670
|
expect.any(String),
|
|
670
671
|
expect.any(Object),
|
|
671
|
-
'update:risks',
|
|
672
|
+
'update:page.risks',
|
|
672
673
|
'risks', // pageId is resource name when appId is available
|
|
673
674
|
true,
|
|
674
675
|
null, // precomputedSuperAdmin
|
|
@@ -677,7 +678,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
677
678
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
678
679
|
expect.any(String),
|
|
679
680
|
expect.any(Object),
|
|
680
|
-
'delete:risks',
|
|
681
|
+
'delete:page.risks',
|
|
681
682
|
'risks', // pageId is resource name when appId is available
|
|
682
683
|
true,
|
|
683
684
|
null, // precomputedSuperAdmin
|
|
@@ -687,7 +688,7 @@ describe('useResourcePermissions Hook', () => {
|
|
|
687
688
|
});
|
|
688
689
|
|
|
689
690
|
describe('Page Permission Support', () => {
|
|
690
|
-
it('
|
|
691
|
+
it('constructs permission strings with page. prefix when appId is available', () => {
|
|
691
692
|
const scopeWithAppId: Scope = {
|
|
692
693
|
organisationId: 'org-123',
|
|
693
694
|
eventId: 'event-123',
|
|
@@ -702,20 +703,38 @@ describe('useResourcePermissions Hook', () => {
|
|
|
702
703
|
|
|
703
704
|
renderHook(() => useResourcePermissions('planning'));
|
|
704
705
|
|
|
705
|
-
// When appId is available,
|
|
706
|
-
//
|
|
706
|
+
// When appId is available, permission strings should include page. prefix
|
|
707
|
+
// and resource name should be passed as pageId to enable page permission checks
|
|
707
708
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
708
709
|
'user-123',
|
|
709
710
|
scopeWithAppId,
|
|
710
|
-
'create:planning',
|
|
711
|
+
'create:page.planning',
|
|
711
712
|
'planning', // Resource name passed as pageId to enable page permission checks
|
|
712
713
|
true,
|
|
713
714
|
null, // precomputedSuperAdmin
|
|
714
715
|
undefined // appName
|
|
715
716
|
);
|
|
717
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
718
|
+
'user-123',
|
|
719
|
+
scopeWithAppId,
|
|
720
|
+
'update:page.planning',
|
|
721
|
+
'planning',
|
|
722
|
+
true,
|
|
723
|
+
null,
|
|
724
|
+
undefined
|
|
725
|
+
);
|
|
726
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
727
|
+
'user-123',
|
|
728
|
+
scopeWithAppId,
|
|
729
|
+
'delete:page.planning',
|
|
730
|
+
'planning',
|
|
731
|
+
true,
|
|
732
|
+
null,
|
|
733
|
+
undefined
|
|
734
|
+
);
|
|
716
735
|
});
|
|
717
736
|
|
|
718
|
-
it('does not
|
|
737
|
+
it('does not add page. prefix when appId is not available', () => {
|
|
719
738
|
const scopeWithoutAppId: Scope = {
|
|
720
739
|
organisationId: 'org-123',
|
|
721
740
|
eventId: 'event-123',
|
|
@@ -730,8 +749,8 @@ describe('useResourcePermissions Hook', () => {
|
|
|
730
749
|
|
|
731
750
|
renderHook(() => useResourcePermissions('planning'));
|
|
732
751
|
|
|
733
|
-
// When appId is not available,
|
|
734
|
-
//
|
|
752
|
+
// When appId is not available, permission strings should NOT include page. prefix
|
|
753
|
+
// and pageId should be undefined - falls back to resource-based permission checking
|
|
735
754
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
736
755
|
'user-123',
|
|
737
756
|
scopeWithoutAppId,
|
|
@@ -741,6 +760,82 @@ describe('useResourcePermissions Hook', () => {
|
|
|
741
760
|
null, // precomputedSuperAdmin
|
|
742
761
|
undefined // appName
|
|
743
762
|
);
|
|
763
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
764
|
+
'user-123',
|
|
765
|
+
scopeWithoutAppId,
|
|
766
|
+
'update:planning',
|
|
767
|
+
undefined,
|
|
768
|
+
true,
|
|
769
|
+
null,
|
|
770
|
+
undefined
|
|
771
|
+
);
|
|
772
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
773
|
+
'user-123',
|
|
774
|
+
scopeWithoutAppId,
|
|
775
|
+
'delete:planning',
|
|
776
|
+
undefined,
|
|
777
|
+
true,
|
|
778
|
+
null,
|
|
779
|
+
undefined
|
|
780
|
+
);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('waits for scope resolution before using page permissions (timing fix)', () => {
|
|
784
|
+
// Simulate scope resolution in progress (resolvedScope is null, isLoading is true)
|
|
785
|
+
mockUseResolvedScope.mockReturnValue({
|
|
786
|
+
resolvedScope: null,
|
|
787
|
+
isLoading: true,
|
|
788
|
+
error: null,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
renderHook(() => useResourcePermissions('planning'));
|
|
792
|
+
|
|
793
|
+
// During scope loading, should use resource-based permissions (no page. prefix)
|
|
794
|
+
// because resolvedScope is null, so hasAppId is false
|
|
795
|
+
const fallbackScope: Scope = {
|
|
796
|
+
organisationId: 'org-123',
|
|
797
|
+
eventId: 'event-123',
|
|
798
|
+
appId: undefined,
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
802
|
+
'user-123',
|
|
803
|
+
fallbackScope,
|
|
804
|
+
'create:planning', // Resource-based permission (no page. prefix) during loading
|
|
805
|
+
undefined, // No pageId when appId is not available
|
|
806
|
+
true,
|
|
807
|
+
null,
|
|
808
|
+
undefined
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// Now simulate scope resolution completing with appId
|
|
812
|
+
const scopeWithAppId: Scope = {
|
|
813
|
+
organisationId: 'org-123',
|
|
814
|
+
eventId: 'event-123',
|
|
815
|
+
appId: 'app-123',
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
mockUseResolvedScope.mockReturnValue({
|
|
819
|
+
resolvedScope: scopeWithAppId,
|
|
820
|
+
isLoading: false,
|
|
821
|
+
error: null,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Re-render to trigger update
|
|
825
|
+
const { rerender } = renderHook(() => useResourcePermissions('planning'));
|
|
826
|
+
rerender();
|
|
827
|
+
|
|
828
|
+
// After scope resolution, should use page permissions (with page. prefix)
|
|
829
|
+
// because resolvedScope.appId is now available, so hasAppId is true
|
|
830
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
831
|
+
'user-123',
|
|
832
|
+
scopeWithAppId,
|
|
833
|
+
'create:page.planning', // Page-based permission (with page. prefix) after resolution
|
|
834
|
+
'planning', // pageId is resource name when appId is available
|
|
835
|
+
true,
|
|
836
|
+
null,
|
|
837
|
+
undefined
|
|
838
|
+
);
|
|
744
839
|
});
|
|
745
840
|
});
|
|
746
841
|
});
|