@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.
Files changed (240) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  3. package/dist/{DataTable-5HITILXS.js → DataTable-5W2HVLLV.js} +8 -8
  4. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
  5. package/dist/{api-5I3E47G2.js → api-SIZPFBFX.js} +5 -3
  6. package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
  7. package/dist/{chunk-P72NKAT5.js → chunk-2BIDKXQU.js} +157 -120
  8. package/dist/chunk-2BIDKXQU.js.map +1 -0
  9. package/dist/{chunk-S4D3Z723.js → chunk-ACYQNYHB.js} +7 -7
  10. package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
  11. package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
  12. package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
  13. package/dist/chunk-IWJYNWXN.js.map +1 -0
  14. package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
  15. package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
  16. package/dist/chunk-MW73E7SP.js.map +1 -0
  17. package/dist/{chunk-F6TSYCKP.js → chunk-PXXS26G5.js} +68 -29
  18. package/dist/chunk-PXXS26G5.js.map +1 -0
  19. package/dist/{chunk-UW2DE6JX.js → chunk-TD4BXGPE.js} +4 -4
  20. package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
  21. package/dist/{chunk-WWNOVFDC.js → chunk-UGVU7L7N.js} +52 -90
  22. package/dist/chunk-UGVU7L7N.js.map +1 -0
  23. package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
  24. package/dist/chunk-X7SPKHYZ.js.map +1 -0
  25. package/dist/{chunk-3TKTL5AZ.js → chunk-ZL45MG76.js} +60 -60
  26. package/dist/chunk-ZL45MG76.js.map +1 -0
  27. package/dist/components.js +10 -10
  28. package/dist/hooks.d.ts +11 -1
  29. package/dist/hooks.js +9 -7
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +13 -13
  33. package/dist/providers.d.ts +2 -2
  34. package/dist/providers.js +2 -2
  35. package/dist/rbac/index.d.ts +46 -29
  36. package/dist/rbac/index.js +9 -9
  37. package/dist/utils.js +1 -1
  38. package/docs/api/classes/ColumnFactory.md +1 -1
  39. package/docs/api/classes/ErrorBoundary.md +1 -1
  40. package/docs/api/classes/InvalidScopeError.md +4 -4
  41. package/docs/api/classes/MissingUserContextError.md +4 -4
  42. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  43. package/docs/api/classes/PermissionDeniedError.md +4 -4
  44. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  45. package/docs/api/classes/RBACAuditManager.md +8 -8
  46. package/docs/api/classes/RBACCache.md +8 -8
  47. package/docs/api/classes/RBACEngine.md +9 -8
  48. package/docs/api/classes/RBACError.md +4 -4
  49. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  50. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  51. package/docs/api/classes/StorageUtils.md +1 -1
  52. package/docs/api/enums/FileCategory.md +1 -1
  53. package/docs/api/interfaces/AggregateConfig.md +1 -1
  54. package/docs/api/interfaces/ButtonProps.md +1 -1
  55. package/docs/api/interfaces/CardProps.md +1 -1
  56. package/docs/api/interfaces/ColorPalette.md +1 -1
  57. package/docs/api/interfaces/ColorShade.md +1 -1
  58. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  59. package/docs/api/interfaces/DataRecord.md +1 -1
  60. package/docs/api/interfaces/DataTableAction.md +1 -1
  61. package/docs/api/interfaces/DataTableColumn.md +1 -1
  62. package/docs/api/interfaces/DataTableProps.md +1 -1
  63. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  64. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  65. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  66. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  67. package/docs/api/interfaces/FileMetadata.md +1 -1
  68. package/docs/api/interfaces/FileReference.md +1 -1
  69. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  70. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  71. package/docs/api/interfaces/FileUploadProps.md +1 -1
  72. package/docs/api/interfaces/FooterProps.md +1 -1
  73. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  74. package/docs/api/interfaces/InputProps.md +1 -1
  75. package/docs/api/interfaces/LabelProps.md +1 -1
  76. package/docs/api/interfaces/LoginFormProps.md +1 -1
  77. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  78. package/docs/api/interfaces/NavigationContextType.md +1 -1
  79. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  80. package/docs/api/interfaces/NavigationItem.md +1 -1
  81. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  82. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  83. package/docs/api/interfaces/Organisation.md +1 -1
  84. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  85. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  86. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  87. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  88. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  89. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  90. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  91. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  92. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  93. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  94. package/docs/api/interfaces/PaletteData.md +1 -1
  95. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  96. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  98. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  99. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  102. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  103. package/docs/api/interfaces/RBACConfig.md +19 -8
  104. package/docs/api/interfaces/RBACLogger.md +5 -5
  105. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  106. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  107. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  108. package/docs/api/interfaces/RouteConfig.md +19 -6
  109. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  110. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  111. package/docs/api/interfaces/StorageConfig.md +1 -1
  112. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  113. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  114. package/docs/api/interfaces/StorageListOptions.md +1 -1
  115. package/docs/api/interfaces/StorageListResult.md +1 -1
  116. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  117. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  118. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  119. package/docs/api/interfaces/StyleImport.md +1 -1
  120. package/docs/api/interfaces/SwitchProps.md +1 -1
  121. package/docs/api/interfaces/ToastActionElement.md +1 -1
  122. package/docs/api/interfaces/ToastProps.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  124. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  126. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  128. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  130. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  133. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  134. package/docs/api/interfaces/UserEventAccess.md +1 -1
  135. package/docs/api/interfaces/UserMenuProps.md +1 -1
  136. package/docs/api/interfaces/UserProfile.md +1 -1
  137. package/docs/api/modules.md +44 -43
  138. package/docs/api-reference/hooks.md +8 -4
  139. package/docs/architecture/rpc-function-standards.md +3 -1
  140. package/docs/best-practices/common-patterns.md +3 -3
  141. package/docs/best-practices/deployment.md +10 -4
  142. package/docs/best-practices/performance.md +11 -3
  143. package/docs/core-concepts/organisations.md +8 -8
  144. package/docs/core-concepts/permissions.md +133 -72
  145. package/docs/documentation-index.md +0 -2
  146. package/docs/migration/rbac-migration.md +65 -66
  147. package/docs/rbac/README.md +114 -38
  148. package/docs/rbac/advanced-patterns.md +15 -22
  149. package/docs/rbac/api-reference.md +63 -16
  150. package/docs/rbac/examples.md +12 -12
  151. package/docs/rbac/getting-started.md +19 -19
  152. package/docs/rbac/quick-start.md +110 -35
  153. package/docs/rbac/troubleshooting.md +127 -3
  154. package/package.json +1 -1
  155. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  156. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  157. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  158. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  159. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  160. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  161. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  162. package/src/components/FileUpload/FileUpload.tsx +2 -8
  163. package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
  164. package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
  165. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  166. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  167. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  168. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  169. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  170. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  171. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  172. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  173. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  174. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  175. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  176. package/src/hooks/index.ts +1 -1
  177. package/src/hooks/useFileDisplay.ts +51 -0
  178. package/src/hooks/usePermissionCache.test.ts +112 -68
  179. package/src/hooks/usePermissionCache.ts +55 -15
  180. package/src/rbac/README.md +81 -39
  181. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  182. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  183. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  184. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  185. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  186. package/src/rbac/adapters.tsx +4 -4
  187. package/src/rbac/api.test.ts +39 -15
  188. package/src/rbac/api.ts +27 -9
  189. package/src/rbac/audit.test.ts +2 -2
  190. package/src/rbac/audit.ts +14 -5
  191. package/src/rbac/cache.test.ts +12 -0
  192. package/src/rbac/cache.ts +29 -9
  193. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  194. package/src/rbac/components/NavigationGuard.tsx +14 -14
  195. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  196. package/src/rbac/components/PagePermissionGuard.tsx +22 -38
  197. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  198. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  199. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  200. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  201. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +2 -2
  202. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  203. package/src/rbac/config.ts +2 -0
  204. package/src/rbac/docs/event-based-apps.md +6 -6
  205. package/src/rbac/engine.ts +27 -7
  206. package/src/rbac/hooks/useCan.test.ts +29 -2
  207. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  208. package/src/rbac/hooks/usePermissions.ts +47 -23
  209. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  210. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  211. package/src/rbac/hooks/useRBAC.ts +0 -55
  212. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  213. package/src/rbac/permissions.test.ts +11 -7
  214. package/src/rbac/security.test.ts +2 -2
  215. package/src/rbac/security.ts +23 -8
  216. package/src/rbac/types.test.ts +2 -2
  217. package/src/rbac/types.ts +1 -2
  218. package/src/services/EventService.ts +41 -13
  219. package/src/services/__tests__/EventService.test.ts +25 -4
  220. package/src/services/interfaces/IEventService.ts +1 -0
  221. package/src/utils/file-reference.ts +9 -0
  222. package/dist/chunk-2W4WKJVF.js.map +0 -1
  223. package/dist/chunk-3TKTL5AZ.js.map +0 -1
  224. package/dist/chunk-AUXS7XSO.js.map +0 -1
  225. package/dist/chunk-F6TSYCKP.js.map +0 -1
  226. package/dist/chunk-P72NKAT5.js.map +0 -1
  227. package/dist/chunk-Q7APDV6H.js.map +0 -1
  228. package/dist/chunk-WWNOVFDC.js.map +0 -1
  229. package/docs/rbac/breaking-changes-v3.md +0 -222
  230. package/docs/rbac/migration-guide.md +0 -260
  231. /package/dist/{DataTable-5HITILXS.js.map → DataTable-5W2HVLLV.js.map} +0 -0
  232. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
  233. /package/dist/{api-5I3E47G2.js.map → api-SIZPFBFX.js.map} +0 -0
  234. /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
  235. /package/dist/{chunk-S4D3Z723.js.map → chunk-ACYQNYHB.js.map} +0 -0
  236. /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
  237. /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
  238. /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
  239. /package/dist/{chunk-UW2DE6JX.js.map → chunk-TD4BXGPE.js.map} +0 -0
  240. /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
@@ -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
- 1. **Never make direct database queries** to `rbac_apps`, `rbac_global_roles`, or other RBAC tables
16
- 2. **Always use `PagePermissionGuard`** for page-level permissions (not manual permission checks)
17
- 3. **Always set up providers correctly** in the exact order shown
18
- 4. **Use the exact app name** from your environment variable (must match database exactly)
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. Check Permissions
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 UserActions() {
214
+ function UsersPage() {
188
215
  return (
189
- <div>
190
- <PagePermissionGuard
191
- pageName="users"
192
- operation="update"
193
- fallback={null}
194
- >
195
- <EditButton />
196
- </PagePermissionGuard>
197
-
198
- <PagePermissionGuard
199
- pageName="users"
200
- operation="delete"
201
- fallback={null}
202
- >
203
- <DeleteButton />
204
- </PagePermissionGuard>
205
- </div>
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
- - `manage` - Full page management
238
-
239
- ### Page-Level Examples
240
- - `read:page.dashboard` - View dashboard page
241
- - `create:page.users` - Create users on users page
242
- - `update:page.settings` - Modify settings page
243
- - `delete:page.admin` - Remove content from admin page
244
- - `manage:page.system` - Full system page management
245
-
246
- ### Event-App Permissions
247
- - `read:events` - View event information
248
- - `create:events` - Create new events
249
- - `update:events` - Modify existing events
250
- - `delete:events` - Remove events
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: 'manage:roles' },
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['manage:roles'] && <RoleManagement />}
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 canManage = await hasPermission('manage:users', {
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
- const mockHasPermission = jest.fn()
596
- .mockResolvedValueOnce(true) // canEditAny
597
- .mockResolvedValueOnce(false); // canDeleteAny
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 canEdit = await result.current.hasPermission('update:users', {
608
- userId: 'user-123',
609
- scope: { organisationId: 'org-456' }
610
- });
600
+ const { result } = renderHook(() => useCan(
601
+ 'user-123',
602
+ { organisationId: 'org-456', eventId: undefined, appId: undefined },
603
+ 'update:users'
604
+ ));
611
605
 
612
- expect(canEdit).toBe(true);
613
- expect(mockHasPermission).toHaveBeenCalledWith('update:users', {
614
- userId: 'user-123',
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
- Conditionally render children based on page-level permissions.
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' | 'write' | 'delete' | 'manage';
256
- children: React.ReactNode;
257
- fallback?: React.ReactNode;
258
- strictMode?: boolean;
259
- auditLog?: boolean;
260
- pageId?: UUID;
261
- scope?: Scope;
262
- onDenied?: (pageName: string, operation: string, reason: string) => void;
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 AdminPage() {
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="admin"
295
+ pageName="users"
277
296
  operation="read"
278
- fallback={<div>Admin access required</div>}
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
- <AdminSettings />
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.
@@ -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('manage:permissions', { userId, scope })
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: 'manage:system' },
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, act } from '@testing-library/react';
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 act(async () => {
703
- const hasPermission = await result.current.hasPermission('read:users', {
704
- userId: 'user-123',
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={['manage:participants']}
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: PermissionEnforcer (Recommended for Pages)
74
- **Use when**: Protecting entire pages or large components
75
- **Benefits**: Automatic scope resolution, no manual context handling
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
- // components/Dashboard.tsx
79
- import { PermissionEnforcer } from '@jmruthers/pace-core/rbac';
78
+ // pages/Dashboard.tsx
79
+ import { PagePermissionGuard } from '@jmruthers/pace-core/rbac';
80
80
 
81
81
  function Dashboard() {
82
82
  return (
83
- <div>
84
- <h1>Dashboard</h1>
85
-
86
- {/* Automatic scope resolution - no manual context needed */}
87
- <PermissionEnforcer
88
- permissions={['read:events']}
89
- operation="dashboard"
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
- </PermissionEnforcer>
94
- </div>
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
- 'manage:users',
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
- 'manage:users'
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
- 'manage:users'
498
+ 'update:users'
499
499
  );
500
500
 
501
501
  if (isLoading) return <div>Checking permissions...</div>;