@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/docs/rbac/README.md
CHANGED
|
@@ -12,10 +12,35 @@ The PACE Core RBAC (Role-Based Access Control) system provides comprehensive per
|
|
|
12
12
|
|
|
13
13
|
## 🚨 Critical Rules (Follow These or It Won't Work)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
**MANDATORY Setup Steps (in order):**
|
|
16
|
+
|
|
17
|
+
1. **Call `setupRBAC(supabase)` FIRST** - Must be called before any RBAC components or hooks
|
|
18
|
+
```typescript
|
|
19
|
+
// In main.tsx or App.tsx
|
|
20
|
+
import { setupRBAC } from '@jmruthers/pace-core/rbac';
|
|
21
|
+
setupRBAC(supabase); // Must be BEFORE rendering App
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. **Wrap app with providers** in exact order:
|
|
25
|
+
```tsx
|
|
26
|
+
<UnifiedAuthProvider supabaseClient={supabase} appName={APP_NAME}>
|
|
27
|
+
<OrganisationProvider>
|
|
28
|
+
<YourApp />
|
|
29
|
+
</OrganisationProvider>
|
|
30
|
+
</UnifiedAuthProvider>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. **Use `PagePermissionGuard` for ALL pages** - This is the ONLY correct way to protect pages
|
|
34
|
+
```tsx
|
|
35
|
+
<PagePermissionGuard pageName="dashboard" operation="read">
|
|
36
|
+
<DashboardContent />
|
|
37
|
+
</PagePermissionGuard>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
4. **Database must be configured** - App, pages, and permissions must exist in database
|
|
41
|
+
5. **User must have organisation role** - Users need roles in `rbac_organisation_roles` table
|
|
42
|
+
6. **App name must match exactly** - Environment variable must match `rbac_apps.name` (case-sensitive)
|
|
43
|
+
7. **Never query RBAC tables directly** - Always use `PagePermissionGuard` or RBAC API functions
|
|
19
44
|
|
|
20
45
|
## 🚀 Quick Start
|
|
21
46
|
|
|
@@ -179,34 +204,58 @@ function App() {
|
|
|
179
204
|
}
|
|
180
205
|
```
|
|
181
206
|
|
|
182
|
-
### 2.
|
|
207
|
+
### 2. Protect Pages with PagePermissionGuard
|
|
208
|
+
|
|
209
|
+
**⚠️ CRITICAL: Always use `PagePermissionGuard` for page-level access. This is the ONLY way to ensure permissions are checked correctly.**
|
|
183
210
|
|
|
184
211
|
```tsx
|
|
185
212
|
import { PagePermissionGuard } from '@jmruthers/pace-core/rbac';
|
|
186
213
|
|
|
187
|
-
function
|
|
214
|
+
function UsersPage() {
|
|
188
215
|
return (
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
>
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
<PagePermissionGuard
|
|
217
|
+
pageName="users"
|
|
218
|
+
operation="read"
|
|
219
|
+
fallback={<div>You don't have permission to view this page</div>}
|
|
220
|
+
>
|
|
221
|
+
<div>
|
|
222
|
+
<h1>User Management</h1>
|
|
223
|
+
|
|
224
|
+
{/* Multiple operations on same page */}
|
|
225
|
+
<PagePermissionGuard
|
|
226
|
+
pageName="users"
|
|
227
|
+
operation="create"
|
|
228
|
+
fallback={null}
|
|
229
|
+
>
|
|
230
|
+
<AddUserButton />
|
|
231
|
+
</PagePermissionGuard>
|
|
232
|
+
|
|
233
|
+
<PagePermissionGuard
|
|
234
|
+
pageName="users"
|
|
235
|
+
operation="update"
|
|
236
|
+
fallback={null}
|
|
237
|
+
>
|
|
238
|
+
<EditUserButtons />
|
|
239
|
+
</PagePermissionGuard>
|
|
240
|
+
|
|
241
|
+
<PagePermissionGuard
|
|
242
|
+
pageName="users"
|
|
243
|
+
operation="delete"
|
|
244
|
+
fallback={null}
|
|
245
|
+
>
|
|
246
|
+
<DeleteUserButtons />
|
|
247
|
+
</PagePermissionGuard>
|
|
248
|
+
</div>
|
|
249
|
+
</PagePermissionGuard>
|
|
206
250
|
);
|
|
207
251
|
}
|
|
208
252
|
```
|
|
209
253
|
|
|
254
|
+
**Important**:
|
|
255
|
+
- `pageName` must match the `page_name` in `rbac_app_pages` table
|
|
256
|
+
- `operation` can be: `read`, `create`, `update`, or `delete`
|
|
257
|
+
- Permission checked in database is: `{operation}:page.{pageName}` (e.g., `read:page.users`)
|
|
258
|
+
|
|
210
259
|
### 3. Protect Components
|
|
211
260
|
|
|
212
261
|
```tsx
|
|
@@ -230,24 +279,51 @@ function AdminPanel() {
|
|
|
230
279
|
The RBAC system uses **page-level permissions** with the format: `{operation}:page.{pageName}`
|
|
231
280
|
|
|
232
281
|
### Operations
|
|
233
|
-
- `read` - View page content
|
|
282
|
+
- `read` - View page content (required for `PagePermissionGuard` with `operation="read"`)
|
|
234
283
|
- `create` - Create new content on page
|
|
235
284
|
- `update` - Modify existing content on page
|
|
236
285
|
- `delete` - Remove content from page
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
- `
|
|
286
|
+
|
|
287
|
+
### Page-Level Permission Format
|
|
288
|
+
|
|
289
|
+
When you use `PagePermissionGuard` with:
|
|
290
|
+
```tsx
|
|
291
|
+
<PagePermissionGuard pageName="dashboard" operation="read">
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The system checks for permission: `read:page.dashboard` in the database.
|
|
295
|
+
|
|
296
|
+
### Database Structure
|
|
297
|
+
|
|
298
|
+
Permissions are stored in `rbac_page_permissions` table with:
|
|
299
|
+
- `app_page_id` - Links to `rbac_app_pages` table
|
|
300
|
+
- `operation` - One of: `read`, `create`, `update`, `delete`
|
|
301
|
+
- `role_name` - User's role (e.g., `org_admin`, `leader`, `member`)
|
|
302
|
+
- `allowed` - Boolean (`true` if user has permission, `false` otherwise)
|
|
303
|
+
- `organisation_id` - Organisation context (must match user's organisation)
|
|
304
|
+
|
|
305
|
+
### Examples
|
|
306
|
+
|
|
307
|
+
If you have a page named `"users"` and check `operation="read"`:
|
|
308
|
+
- System checks: `read:page.users` permission
|
|
309
|
+
- Database query looks in `rbac_page_permissions` for matching `operation='read'` and `page_name='users'`
|
|
310
|
+
- Permission is granted if user's role has `allowed=true` for that page, operation, and organisation
|
|
311
|
+
|
|
312
|
+
### Complete Example
|
|
313
|
+
|
|
314
|
+
```sql
|
|
315
|
+
-- Database setup for a "users" page with read permission for org_admin role
|
|
316
|
+
INSERT INTO rbac_page_permissions (app_page_id, operation, role_name, allowed, organisation_id)
|
|
317
|
+
VALUES (
|
|
318
|
+
(SELECT id FROM rbac_app_pages WHERE page_name = 'users'),
|
|
319
|
+
'read',
|
|
320
|
+
'org_admin',
|
|
321
|
+
true,
|
|
322
|
+
'your-organisation-id'::uuid
|
|
323
|
+
);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
This allows users with `org_admin` role to access `<PagePermissionGuard pageName="users" operation="read">`.
|
|
251
327
|
|
|
252
328
|
## 🔒 Security Features
|
|
253
329
|
|
|
@@ -29,7 +29,7 @@ function UserManagementDashboard({ userId, scope }) {
|
|
|
29
29
|
{ userId, scope, permission: 'create:users' },
|
|
30
30
|
{ userId, scope, permission: 'update:users' },
|
|
31
31
|
{ userId, scope, permission: 'delete:users' },
|
|
32
|
-
{ userId, scope, permission: '
|
|
32
|
+
{ userId, scope, permission: 'update:roles' },
|
|
33
33
|
{ userId, scope, permission: 'read:audit_logs' }
|
|
34
34
|
]);
|
|
35
35
|
|
|
@@ -52,7 +52,7 @@ function UserManagementDashboard({ userId, scope }) {
|
|
|
52
52
|
{permissions['create:users'] && <CreateUserButton />}
|
|
53
53
|
{permissions['update:users'] && <EditUserButton />}
|
|
54
54
|
{permissions['delete:users'] && <DeleteUserButton />}
|
|
55
|
-
{permissions['
|
|
55
|
+
{permissions['update:roles'] && <RoleManagement />}
|
|
56
56
|
{permissions['read:audit_logs'] && <AuditLogs />}
|
|
57
57
|
</div>
|
|
58
58
|
);
|
|
@@ -289,7 +289,7 @@ function HierarchicalUserManagement({ userId, scope, users }) {
|
|
|
289
289
|
const permissionChecks = await Promise.all(
|
|
290
290
|
users.map(async (user) => {
|
|
291
291
|
// Check if current user can manage this user based on hierarchy
|
|
292
|
-
const
|
|
292
|
+
const canUpdate = await hasPermission('update:users', {
|
|
293
293
|
userId,
|
|
294
294
|
scope: {
|
|
295
295
|
...scope,
|
|
@@ -592,27 +592,20 @@ describe('Advanced Permission Patterns', () => {
|
|
|
592
592
|
});
|
|
593
593
|
|
|
594
594
|
test('should handle conditional permissions', async () => {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
.
|
|
598
|
-
|
|
599
|
-
(useCan as jest.Mock).mockReturnValue({
|
|
600
|
-
hasPermission: mockHasPermission,
|
|
601
|
-
isLoading: false,
|
|
602
|
-
error: null
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
const { result } = renderHook(() => useCan());
|
|
595
|
+
// Mock the underlying permission API
|
|
596
|
+
vi.mock('../api', () => ({
|
|
597
|
+
isPermittedCached: vi.fn().mockResolvedValue(true)
|
|
598
|
+
}));
|
|
606
599
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
600
|
+
const { result } = renderHook(() => useCan(
|
|
601
|
+
'user-123',
|
|
602
|
+
{ organisationId: 'org-456', eventId: undefined, appId: undefined },
|
|
603
|
+
'update:users'
|
|
604
|
+
));
|
|
611
605
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
scope: { organisationId: 'org-456' }
|
|
606
|
+
await waitFor(() => {
|
|
607
|
+
expect(result.current.isLoading).toBe(false);
|
|
608
|
+
expect(result.current.can).toBe(true);
|
|
616
609
|
});
|
|
617
610
|
});
|
|
618
611
|
|
|
@@ -247,43 +247,90 @@ function UserActions() {
|
|
|
247
247
|
|
|
248
248
|
### PagePermissionGuard
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
**⚠️ CRITICAL: This is the PRIMARY component for protecting pages.** Always use `PagePermissionGuard` for page-level permissions. It automatically resolves scope from context and checks permissions in the database.
|
|
251
|
+
|
|
252
|
+
**Permission Format**: The component checks for permissions in the format `{operation}:page.{pageName}` (e.g., `read:page.dashboard`).
|
|
251
253
|
|
|
252
254
|
```typescript
|
|
253
255
|
interface PagePermissionGuardProps {
|
|
254
|
-
pageName: string;
|
|
255
|
-
operation: 'read' | '
|
|
256
|
-
children: React.ReactNode;
|
|
257
|
-
fallback?: React.ReactNode;
|
|
258
|
-
strictMode?: boolean;
|
|
259
|
-
auditLog?: boolean;
|
|
260
|
-
pageId?:
|
|
261
|
-
scope?: Scope;
|
|
262
|
-
onDenied?: (pageName: string, operation: string
|
|
263
|
-
loading?: React.ReactNode;
|
|
256
|
+
pageName: string; // Page name (e.g., "dashboard", "users") - must match rbac_app_pages.page_name
|
|
257
|
+
operation: 'read' | 'create' | 'update' | 'delete'; // Operation being performed
|
|
258
|
+
children: React.ReactNode; // Content to render if user has permission
|
|
259
|
+
fallback?: React.ReactNode; // Content to render if user lacks permission (default: Access Denied component)
|
|
260
|
+
strictMode?: boolean; // Enable strict mode for security logging (default: true)
|
|
261
|
+
auditLog?: boolean; // Enable audit logging (default: true)
|
|
262
|
+
pageId?: string; // Optional page ID override (defaults to pageName)
|
|
263
|
+
scope?: Scope; // Optional scope override (defaults to resolving from context)
|
|
264
|
+
onDenied?: (pageName: string, operation: string) => void; // Callback when access denied
|
|
265
|
+
loading?: React.ReactNode; // Loading state component (default: "Checking permissions...")
|
|
264
266
|
}
|
|
265
267
|
```
|
|
266
268
|
|
|
267
|
-
#### Usage
|
|
269
|
+
#### Basic Usage
|
|
268
270
|
|
|
269
271
|
```tsx
|
|
270
272
|
import { PagePermissionGuard } from '@jmruthers/pace-core/rbac';
|
|
271
273
|
|
|
272
|
-
function
|
|
274
|
+
function DashboardPage() {
|
|
275
|
+
return (
|
|
276
|
+
<PagePermissionGuard
|
|
277
|
+
pageName="dashboard"
|
|
278
|
+
operation="read"
|
|
279
|
+
fallback={<div>You don't have permission to view the dashboard</div>}
|
|
280
|
+
>
|
|
281
|
+
<DashboardContent />
|
|
282
|
+
</PagePermissionGuard>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### Multiple Operations
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
function UsersPage() {
|
|
273
291
|
return (
|
|
274
292
|
<div>
|
|
293
|
+
{/* Read permission - show user list */}
|
|
275
294
|
<PagePermissionGuard
|
|
276
|
-
pageName="
|
|
295
|
+
pageName="users"
|
|
277
296
|
operation="read"
|
|
278
|
-
fallback={
|
|
297
|
+
fallback={null}
|
|
298
|
+
>
|
|
299
|
+
<UserList />
|
|
300
|
+
</PagePermissionGuard>
|
|
301
|
+
|
|
302
|
+
{/* Create permission - show add button */}
|
|
303
|
+
<PagePermissionGuard
|
|
304
|
+
pageName="users"
|
|
305
|
+
operation="create"
|
|
306
|
+
fallback={null}
|
|
279
307
|
>
|
|
280
|
-
<
|
|
308
|
+
<AddUserButton />
|
|
309
|
+
</PagePermissionGuard>
|
|
310
|
+
|
|
311
|
+
{/* Update permission - show edit buttons */}
|
|
312
|
+
<PagePermissionGuard
|
|
313
|
+
pageName="users"
|
|
314
|
+
operation="update"
|
|
315
|
+
fallback={null}
|
|
316
|
+
>
|
|
317
|
+
<EditUserButtons />
|
|
281
318
|
</PagePermissionGuard>
|
|
282
319
|
</div>
|
|
283
320
|
);
|
|
284
321
|
}
|
|
285
322
|
```
|
|
286
323
|
|
|
324
|
+
#### Important Notes
|
|
325
|
+
|
|
326
|
+
1. **Automatic Scope Resolution**: `PagePermissionGuard` automatically resolves `organisationId` from `UnifiedAuthProvider` context. You don't need to pass `scope` unless you need custom scope.
|
|
327
|
+
|
|
328
|
+
2. **Permission Format**: The component checks for `{operation}:page.{pageName}` in the database. Ensure your `rbac_page_permissions` table has entries matching this format.
|
|
329
|
+
|
|
330
|
+
3. **Loading State**: The component shows a loading state while checking permissions. Provide a custom `loading` prop if needed.
|
|
331
|
+
|
|
332
|
+
4. **Error Handling**: If permission check fails, the component shows the `fallback`. Check browser console for `[PagePermissionGuard]` logs to debug issues.
|
|
333
|
+
|
|
287
334
|
### NavigationGuard
|
|
288
335
|
|
|
289
336
|
Conditionally render navigation items based on permissions.
|
package/docs/rbac/examples.md
CHANGED
|
@@ -234,7 +234,7 @@ function UserForm({ userId, scope, userData }) {
|
|
|
234
234
|
if (!isLoading) {
|
|
235
235
|
const [canEditRole, canEditPermissions] = await Promise.all([
|
|
236
236
|
hasPermission('update:user_roles', { userId, scope }),
|
|
237
|
-
hasPermission('
|
|
237
|
+
hasPermission('update:permissions', { userId, scope })
|
|
238
238
|
]);
|
|
239
239
|
|
|
240
240
|
setPermissions({ canEditRole, canEditPermissions });
|
|
@@ -295,7 +295,7 @@ function Navigation({ userId, scope }) {
|
|
|
295
295
|
{ id: 'users', label: 'Users', permission: 'read:users' },
|
|
296
296
|
{ id: 'events', label: 'Events', permission: 'read:events' },
|
|
297
297
|
{ id: 'settings', label: 'Settings', permission: 'read:settings' },
|
|
298
|
-
{ id: 'admin', label: 'Admin', permission: '
|
|
298
|
+
{ id: 'admin', label: 'Admin', permission: 'update:system' },
|
|
299
299
|
];
|
|
300
300
|
|
|
301
301
|
useEffect(() => {
|
|
@@ -693,19 +693,19 @@ test('renders fallback when user lacks permission', () => {
|
|
|
693
693
|
### Testing Hooks
|
|
694
694
|
|
|
695
695
|
```tsx
|
|
696
|
-
import { renderHook,
|
|
696
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
697
697
|
import { useCan } from '@jmruthers/pace-core/rbac';
|
|
698
698
|
|
|
699
699
|
test('useCan hook returns permission result', async () => {
|
|
700
|
-
const { result } = renderHook(() => useCan(
|
|
700
|
+
const { result } = renderHook(() => useCan(
|
|
701
|
+
'user-123',
|
|
702
|
+
{ organisationId: 'org-456', eventId: undefined, appId: undefined },
|
|
703
|
+
'read:users'
|
|
704
|
+
));
|
|
701
705
|
|
|
702
|
-
await
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
scope: { organisationId: 'org-456' }
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
expect(hasPermission).toBe(true);
|
|
706
|
+
await waitFor(() => {
|
|
707
|
+
expect(result.current.isLoading).toBe(false);
|
|
708
|
+
expect(result.current.can).toBe(true);
|
|
709
709
|
});
|
|
710
710
|
});
|
|
711
711
|
```
|
|
@@ -765,7 +765,7 @@ function EventDashboard() {
|
|
|
765
765
|
</PermissionEnforcer>
|
|
766
766
|
|
|
767
767
|
<PermissionEnforcer
|
|
768
|
-
permissions={['
|
|
768
|
+
permissions={['update:participants']}
|
|
769
769
|
operation="participant-management"
|
|
770
770
|
fallback={null}
|
|
771
771
|
>
|
|
@@ -70,32 +70,32 @@ export default App;
|
|
|
70
70
|
|
|
71
71
|
## 🎯 **When to Use What Pattern**
|
|
72
72
|
|
|
73
|
-
### Pattern 1:
|
|
74
|
-
**Use when**: Protecting entire pages or
|
|
75
|
-
**Benefits**: Automatic scope resolution,
|
|
73
|
+
### Pattern 1: PagePermissionGuard (REQUIRED for Pages)
|
|
74
|
+
**Use when**: Protecting entire pages or page-level access
|
|
75
|
+
**Benefits**: Automatic scope resolution, page-level permission checking, strict mode enforcement
|
|
76
76
|
|
|
77
77
|
```tsx
|
|
78
|
-
//
|
|
79
|
-
import {
|
|
78
|
+
// pages/Dashboard.tsx
|
|
79
|
+
import { PagePermissionGuard } from '@jmruthers/pace-core/rbac';
|
|
80
80
|
|
|
81
81
|
function Dashboard() {
|
|
82
82
|
return (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fallback={<div>Access Denied</div>}
|
|
91
|
-
>
|
|
83
|
+
<PagePermissionGuard
|
|
84
|
+
pageName="dashboard"
|
|
85
|
+
operation="read"
|
|
86
|
+
fallback={<div>Access Denied</div>}
|
|
87
|
+
>
|
|
88
|
+
<div>
|
|
89
|
+
<h1>Dashboard</h1>
|
|
92
90
|
<EventContent />
|
|
93
|
-
</
|
|
94
|
-
</
|
|
91
|
+
</div>
|
|
92
|
+
</PagePermissionGuard>
|
|
95
93
|
);
|
|
96
94
|
}
|
|
97
95
|
```
|
|
98
96
|
|
|
97
|
+
**⚠️ CRITICAL**: Always use `PagePermissionGuard` for pages. It's the only component that properly checks page-level permissions against the database.
|
|
98
|
+
|
|
99
99
|
### Pattern 2: usePermissions (For Data Fetching)
|
|
100
100
|
**Use when**: You need the actual permission data for custom logic
|
|
101
101
|
**Requirements**: Must provide explicit scope
|
|
@@ -150,7 +150,7 @@ function UserActions() {
|
|
|
150
150
|
eventId: undefined,
|
|
151
151
|
appId: undefined
|
|
152
152
|
},
|
|
153
|
-
'
|
|
153
|
+
'update:users',
|
|
154
154
|
'page-123' // optional pageId
|
|
155
155
|
);
|
|
156
156
|
|
|
@@ -239,7 +239,7 @@ function UserManagement({ userId, organisationId }) {
|
|
|
239
239
|
const { can, isLoading, error } = useCan(
|
|
240
240
|
userId || '',
|
|
241
241
|
{ organisationId: organisationId || '' },
|
|
242
|
-
'
|
|
242
|
+
'update:users'
|
|
243
243
|
);
|
|
244
244
|
|
|
245
245
|
if (isLoading) return <div>Checking permissions...</div>;
|
|
@@ -495,7 +495,7 @@ function MyComponent() {
|
|
|
495
495
|
const { can, isLoading, error } = useCan(
|
|
496
496
|
user?.id || '',
|
|
497
497
|
{ organisationId: selectedOrganisationId || '' },
|
|
498
|
-
'
|
|
498
|
+
'update:users'
|
|
499
499
|
);
|
|
500
500
|
|
|
501
501
|
if (isLoading) return <div>Checking permissions...</div>;
|