@jmruthers/pace-core 0.5.108 → 0.5.110

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 (195) hide show
  1. package/CHANGELOG.md +93 -173
  2. package/dist/{AuthService-1D2ifNfa.d.ts → AuthService-DrHrvXNZ.d.ts} +8 -1
  3. package/dist/{DataTable-WFCHVWTY.js → DataTable-D3BK2FCN.js} +7 -7
  4. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js → UnifiedAuthProvider-A7I23UCN.js} +3 -3
  5. package/dist/{api-KG4A2X7P.js → api-PIE4JRFS.js} +2 -2
  6. package/dist/{chunk-DMNMZKWS.js → chunk-2W4WKJVF.js} +4 -4
  7. package/dist/{chunk-B3QX32P5.js → chunk-3J5N2T2N.js} +85 -28
  8. package/dist/chunk-3J5N2T2N.js.map +1 -0
  9. package/dist/{chunk-MOMYOQMC.js → chunk-7GBEBJLR.js} +29 -37
  10. package/dist/chunk-7GBEBJLR.js.map +1 -0
  11. package/dist/{chunk-X4FRXJV6.js → chunk-AUXS7XSO.js} +57 -6
  12. package/dist/{chunk-X4FRXJV6.js.map → chunk-AUXS7XSO.js.map} +1 -1
  13. package/dist/{chunk-VJ7MPS2K.js → chunk-AWK2FAUN.js} +6 -6
  14. package/dist/{chunk-LT6RKRA7.js → chunk-D6MEKC27.js} +2 -2
  15. package/dist/{chunk-KBG34SVL.js → chunk-EYSXQ756.js} +2 -2
  16. package/dist/{chunk-ZXY5NTJB.js → chunk-EZ64QG2I.js} +2 -2
  17. package/dist/chunk-GZRXOUBE.js +176 -0
  18. package/dist/chunk-GZRXOUBE.js.map +1 -0
  19. package/dist/{chunk-QDDUU625.js → chunk-HADXAZT3.js} +4 -4
  20. package/dist/{chunk-IMZGJ2X7.js → chunk-HGZSO43Y.js} +4 -4
  21. package/dist/{chunk-S63MFSY6.js → chunk-XRSP3H52.js} +15 -8
  22. package/dist/chunk-XRSP3H52.js.map +1 -0
  23. package/dist/{chunk-GVRSXXAA.js → chunk-YFMENCR4.js} +3 -3
  24. package/dist/components.js +9 -9
  25. package/dist/{database-BXAfr2Y_.d.ts → database-C6jy7EOu.d.ts} +21 -9
  26. package/dist/{formatting-BiEv5oEk.d.ts → formatting-B1jSqgl-.d.ts} +16 -1
  27. package/dist/hooks.d.ts +2 -2
  28. package/dist/hooks.js +7 -7
  29. package/dist/index.d.ts +6 -6
  30. package/dist/index.js +16 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/providers.d.ts +4 -3
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +35 -23
  35. package/dist/rbac/index.js +8 -8
  36. package/dist/types.d.ts +2 -2
  37. package/dist/{usePublicRouteParams-CnM-IK2I.d.ts → usePublicRouteParams-BdF8bZgs.d.ts} +1 -1
  38. package/dist/utils.d.ts +2 -15
  39. package/dist/utils.js +4 -145
  40. package/dist/utils.js.map +1 -1
  41. package/dist/validation.d.ts +1 -1
  42. package/docs/api/classes/ColumnFactory.md +1 -1
  43. package/docs/api/classes/ErrorBoundary.md +1 -1
  44. package/docs/api/classes/InvalidScopeError.md +1 -1
  45. package/docs/api/classes/MissingUserContextError.md +1 -1
  46. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  47. package/docs/api/classes/PermissionDeniedError.md +1 -1
  48. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  49. package/docs/api/classes/RBACAuditManager.md +1 -1
  50. package/docs/api/classes/RBACCache.md +1 -1
  51. package/docs/api/classes/RBACEngine.md +9 -8
  52. package/docs/api/classes/RBACError.md +1 -1
  53. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  54. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  55. package/docs/api/classes/StorageUtils.md +1 -1
  56. package/docs/api/enums/FileCategory.md +1 -1
  57. package/docs/api/interfaces/AggregateConfig.md +1 -1
  58. package/docs/api/interfaces/ButtonProps.md +1 -1
  59. package/docs/api/interfaces/CardProps.md +1 -1
  60. package/docs/api/interfaces/ColorPalette.md +1 -1
  61. package/docs/api/interfaces/ColorShade.md +1 -1
  62. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  63. package/docs/api/interfaces/DataRecord.md +1 -1
  64. package/docs/api/interfaces/DataTableAction.md +1 -1
  65. package/docs/api/interfaces/DataTableColumn.md +3 -3
  66. package/docs/api/interfaces/DataTableProps.md +1 -1
  67. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  68. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  69. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  70. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  71. package/docs/api/interfaces/FileMetadata.md +1 -1
  72. package/docs/api/interfaces/FileReference.md +1 -1
  73. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  74. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  82. package/docs/api/interfaces/NavigationContextType.md +1 -1
  83. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  95. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  100. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  102. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  103. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  106. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  107. package/docs/api/interfaces/RBACConfig.md +19 -8
  108. package/docs/api/interfaces/RBACLogger.md +5 -5
  109. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  110. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  111. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  112. package/docs/api/interfaces/RouteConfig.md +1 -1
  113. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  114. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  115. package/docs/api/interfaces/StorageConfig.md +1 -1
  116. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  117. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  118. package/docs/api/interfaces/StorageListOptions.md +1 -1
  119. package/docs/api/interfaces/StorageListResult.md +1 -1
  120. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  121. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  122. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  123. package/docs/api/interfaces/StyleImport.md +1 -1
  124. package/docs/api/interfaces/SwitchProps.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  137. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  138. package/docs/api/interfaces/UserEventAccess.md +1 -1
  139. package/docs/api/interfaces/UserMenuProps.md +1 -1
  140. package/docs/api/interfaces/UserProfile.md +1 -1
  141. package/docs/api/modules.md +55 -20
  142. package/docs/api-reference/hooks.md +53 -0
  143. package/docs/api-reference/providers.md +60 -0
  144. package/docs/core-concepts/authentication.md +2 -0
  145. package/docs/documentation-index.md +0 -2
  146. package/docs/implementation-guides/authentication.md +1 -0
  147. package/docs/rbac/README.md +114 -38
  148. package/docs/rbac/api-reference.md +63 -16
  149. package/docs/rbac/getting-started.md +16 -16
  150. package/docs/rbac/quick-start.md +110 -35
  151. package/docs/rbac/troubleshooting.md +125 -2
  152. package/docs/security/README.md +59 -0
  153. package/package.json +1 -1
  154. package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
  155. package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
  156. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +2 -2
  157. package/src/components/PaceAppLayout/PaceAppLayout.tsx +48 -16
  158. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +2 -1
  159. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +9 -9
  160. package/src/index.ts +3 -0
  161. package/src/providers/services/AuthServiceProvider.tsx +4 -3
  162. package/src/providers/services/UnifiedAuthProvider.tsx +1 -1
  163. package/src/rbac/api.test.ts +2 -2
  164. package/src/rbac/api.ts +2 -1
  165. package/src/rbac/components/PagePermissionGuard.tsx +21 -38
  166. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  167. package/src/rbac/config.ts +2 -0
  168. package/src/rbac/engine.ts +17 -5
  169. package/src/rbac/security.ts +1 -1
  170. package/src/services/AuthService.ts +79 -1
  171. package/src/services/__tests__/AuthService.test.ts +184 -0
  172. package/src/types/database.ts +21 -9
  173. package/src/types/rbac-functions.ts +2 -1
  174. package/src/utils/__tests__/sessionTracking.unit.test.ts +6 -171
  175. package/src/utils/sessionTracking.ts +7 -81
  176. package/dist/chunk-B3QX32P5.js.map +0 -1
  177. package/dist/chunk-MOMYOQMC.js.map +0 -1
  178. package/dist/chunk-NFPV7MRN.js +0 -94
  179. package/dist/chunk-NFPV7MRN.js.map +0 -1
  180. package/dist/chunk-S63MFSY6.js.map +0 -1
  181. package/docs/rbac/breaking-changes-v3.md +0 -222
  182. package/docs/rbac/migration-guide.md +0 -260
  183. package/src/providers/AuthProvider.simplified.tsx +0 -974
  184. package/dist/{DataTable-WFCHVWTY.js.map → DataTable-D3BK2FCN.js.map} +0 -0
  185. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js.map → UnifiedAuthProvider-A7I23UCN.js.map} +0 -0
  186. package/dist/{api-KG4A2X7P.js.map → api-PIE4JRFS.js.map} +0 -0
  187. package/dist/{chunk-DMNMZKWS.js.map → chunk-2W4WKJVF.js.map} +0 -0
  188. package/dist/{chunk-VJ7MPS2K.js.map → chunk-AWK2FAUN.js.map} +0 -0
  189. package/dist/{chunk-LT6RKRA7.js.map → chunk-D6MEKC27.js.map} +0 -0
  190. package/dist/{chunk-KBG34SVL.js.map → chunk-EYSXQ756.js.map} +0 -0
  191. package/dist/{chunk-ZXY5NTJB.js.map → chunk-EZ64QG2I.js.map} +0 -0
  192. package/dist/{chunk-QDDUU625.js.map → chunk-HADXAZT3.js.map} +0 -0
  193. package/dist/{chunk-IMZGJ2X7.js.map → chunk-HGZSO43Y.js.map} +0 -0
  194. package/dist/{chunk-GVRSXXAA.js.map → chunk-YFMENCR4.js.map} +0 -0
  195. package/dist/{validation-D8VcbTzC.d.ts → validation-DnhrNMju.d.ts} +2 -2
@@ -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.
@@ -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
@@ -25,9 +25,11 @@ A simple user management app that demonstrates:
25
25
 
26
26
  1. **ALWAYS call `setupRBAC()` first** - This is MANDATORY and must be called before any RBAC features
27
27
  2. **Never make direct database queries** to `rbac_apps`, `rbac_global_roles`, or other RBAC tables
28
- 3. **Always use `PagePermissionGuard`** for page-level permissions (not manual permission checks)
29
- 4. **Always set up providers correctly** in the exact order shown
30
- 5. **Use the exact app name** from your environment variable (must match database exactly)
28
+ 3. **ALWAYS use `PagePermissionGuard`** for page-level permissions - This is the ONLY way to properly protect pages
29
+ 4. **ALWAYS set up providers correctly** in the exact order shown below
30
+ 5. **Use the exact app name** from your environment variable (must match database exactly, case-sensitive)
31
+ 6. **Ensure database setup is complete** before starting the app - App must be registered with pages and permissions
32
+ 7. **User must have organisation role** - Users need roles assigned in `rbac_organisation_roles` table
31
33
 
32
34
  ## 🚀 Step-by-Step Implementation
33
35
 
@@ -127,8 +129,11 @@ WHERE a.name = 'user-manager';
127
129
 
128
130
  #### 5.3 Set Up Page Permissions
129
131
 
132
+ **CRITICAL**: Page permissions use the format `{operation}:page.{pageName}` (e.g., `read:page.dashboard`). The database stores permissions per page per role per organisation.
133
+
130
134
  ```sql
131
135
  -- Set up basic permissions for each page (replace 'user-manager' with your actual app name)
136
+ -- IMPORTANT: Replace the organisation_id UUID with your actual organisation ID
132
137
  WITH app_pages AS (
133
138
  SELECT ap.id as page_id, ap.page_name, a.id as app_id
134
139
  FROM rbac_app_pages ap
@@ -146,10 +151,24 @@ SELECT
146
151
  WHEN role.role_name = 'member' AND ap.page_name IN ('dashboard', 'users') THEN true
147
152
  ELSE false
148
153
  END,
149
- '00000000-0000-0000-0000-000000000000'::uuid -- Replace with your organisation ID
154
+ '00000000-0000-0000-0000-000000000000'::uuid -- ⚠️ REPLACE THIS with your actual organisation ID
150
155
  FROM app_pages ap
151
156
  CROSS JOIN (SELECT unnest(ARRAY['read', 'create', 'update', 'delete']) as operation) op
152
157
  CROSS JOIN (SELECT unnest(ARRAY['org_admin', 'leader', 'member']) as role_name) role;
158
+
159
+ -- Verify permissions were created correctly
160
+ SELECT
161
+ a.name as app_name,
162
+ ap.page_name,
163
+ pp.operation,
164
+ pp.role_name,
165
+ pp.allowed,
166
+ pp.organisation_id
167
+ FROM rbac_page_permissions pp
168
+ JOIN rbac_app_pages ap ON pp.app_page_id = ap.id
169
+ JOIN rbac_apps a ON ap.app_id = a.id
170
+ WHERE a.name = 'user-manager'
171
+ ORDER BY ap.page_name, pp.operation, pp.role_name;
153
172
  ```
154
173
 
155
174
  #### 5.4 Assign User Roles
@@ -202,28 +221,23 @@ if (!supabaseUrl || !supabaseAnonKey) {
202
221
  export const supabase = createClient(supabaseUrl, supabaseAnonKey)
203
222
  ```
204
223
 
205
- ### 8. Initialize RBAC (MANDATORY - NEW STEP!)
224
+ ### 8. Initialize RBAC (MANDATORY!)
206
225
 
207
- **CRITICAL**: You MUST call `setupRBAC()` before using any RBAC features. Create `src/lib/rbac-setup.ts`:
226
+ **CRITICAL**: You MUST call `setupRBAC()` before using any RBAC features. This configures rate limiting (default: 1000 requests/minute) and enables security validation.
208
227
 
209
- ```typescript
210
- // src/lib/rbac-setup.ts
211
- import { setupRBAC } from '@jmruthers/pace-core/rbac'
212
- import { supabase } from './supabase'
213
-
214
- // ⚠️ REQUIRED: Initialize RBAC before using any RBAC features
215
- setupRBAC(supabase)
216
- ```
217
-
218
- **Then import this in your main entry point:**
228
+ **Option 1: Setup in main entry point (Recommended)**
219
229
 
220
230
  ```typescript
221
231
  // src/main.tsx
222
232
  import React from 'react'
223
233
  import ReactDOM from 'react-dom/client'
224
- import './lib/rbac-setup' // ⚠️ Import setup BEFORE App
234
+ import { setupRBAC } from '@jmruthers/pace-core/rbac'
235
+ import { supabase } from './lib/supabase'
225
236
  import App from './App'
226
237
 
238
+ // ⚠️ CRITICAL: Call setupRBAC BEFORE rendering App
239
+ setupRBAC(supabase)
240
+
227
241
  ReactDOM.createRoot(document.getElementById('root')!).render(
228
242
  <React.StrictMode>
229
243
  <App />
@@ -231,18 +245,24 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
231
245
  )
232
246
  ```
233
247
 
234
- **Or in your App.tsx (alternative approach):**
248
+ **Option 2: Setup in separate file (Alternative)**
235
249
 
236
250
  ```typescript
237
- // src/App.tsx
238
- import React from 'react'
251
+ // src/lib/rbac-setup.ts
239
252
  import { setupRBAC } from '@jmruthers/pace-core/rbac'
240
- import { supabase } from './lib/supabase'
253
+ import { supabase } from './supabase'
241
254
 
242
- // ⚠️ REQUIRED: Call setupRBAC before anything else
243
- setupRBAC(supabase)
255
+ // ⚠️ REQUIRED: Initialize RBAC before using any RBAC features
256
+ setupRBAC(supabase, {
257
+ // Optional: Configure rate limiting for high-traffic apps
258
+ security: {
259
+ maxPermissionChecksPerMinute: 2000 // Increase from default 1000 if needed
260
+ }
261
+ })
244
262
 
245
- // ... rest of your app
263
+ // src/main.tsx
264
+ import './lib/rbac-setup' // Import BEFORE App
265
+ import App from './App'
246
266
  ```
247
267
 
248
268
  ### 9. App Setup (CRITICAL)
@@ -560,12 +580,25 @@ export function Users() {
560
580
 
561
581
  Your RBAC setup is working correctly if:
562
582
 
583
+ **Setup Checklist:**
584
+ - [ ] `setupRBAC(supabase)` is called before rendering App
585
+ - [ ] App is registered in `rbac_apps` table with `is_active = true`
586
+ - [ ] Pages exist in `rbac_app_pages` for your app
587
+ - [ ] Page permissions exist in `rbac_page_permissions` for your pages, roles, and organisation
588
+ - [ ] User has an active role in `rbac_organisation_roles` for the organisation
589
+ - [ ] `VITE_APP_NAME` environment variable matches database `rbac_apps.name` exactly
590
+
591
+ **Runtime Checklist:**
563
592
  - [ ] You can log in successfully
593
+ - [ ] Browser console shows "RBAC system initialized successfully"
564
594
  - [ ] You see the dashboard without "Access Denied" messages
595
+ - [ ] `PagePermissionGuard` shows loading state, then renders content (not fallback)
565
596
  - [ ] You can navigate to the users page
566
597
  - [ ] You see "Success! Your RBAC setup is working correctly" on the users page
567
598
  - [ ] No 400/406 errors in the browser console
568
599
  - [ ] No "App not found" errors in the console
600
+ - [ ] No "STRICT MODE VIOLATION" errors in the console
601
+ - [ ] No "rate_limit_exceeded" errors (if you see these, increase rate limit in setupRBAC)
569
602
 
570
603
  ## 🚨 Troubleshooting
571
604
 
@@ -579,33 +612,75 @@ import { setupRBAC } from '@jmruthers/pace-core/rbac'
579
612
  setupRBAC(supabase) // Must be called BEFORE rendering app
580
613
  ```
581
614
 
582
- ### Issue: "Access Denied" on all pages
615
+ ### Issue: "Access Denied" or "STRICT MODE VIOLATION" on all pages
616
+
617
+ **This is the most common issue. Check these in EXACT ORDER:**
583
618
 
584
- **Check these in order:**
619
+ 1. **Verify setupRBAC was called**:
620
+ - Check browser console for "RBAC system initialized successfully"
621
+ - If missing, add `setupRBAC(supabase)` in your main.tsx before rendering
585
622
 
586
- 1. **Verify your app is registered**:
623
+ 2. **Verify your app is registered and active**:
587
624
  ```sql
588
- SELECT * FROM rbac_apps WHERE name = 'user-manager';
625
+ SELECT id, name, display_name, requires_event, is_active
626
+ FROM rbac_apps
627
+ WHERE name = 'user-manager';
589
628
  ```
629
+ - Must return exactly 1 row
630
+ - `is_active` must be `true`
631
+ - `name` must match your `VITE_APP_NAME` exactly (case-sensitive)
590
632
 
591
- 2. **Check your environment variable**:
633
+ 3. **Check your environment variable matches database**:
592
634
  ```typescript
593
- console.log('App name:', import.meta.env.VITE_APP_NAME);
635
+ console.log('App name from env:', import.meta.env.VITE_APP_NAME);
636
+ ```
637
+ - Must match database `rbac_apps.name` exactly
638
+
639
+ 4. **Verify pages exist for your app**:
640
+ ```sql
641
+ SELECT ap.id, ap.page_name, ap.page_description
642
+ FROM rbac_app_pages ap
643
+ JOIN rbac_apps a ON ap.app_id = a.id
644
+ WHERE a.name = 'user-manager'
645
+ ORDER BY ap.page_name;
594
646
  ```
647
+ - Must have pages for `dashboard`, `users`, etc.
595
648
 
596
- 3. **Verify user has roles**:
649
+ 5. **Verify user has organisation role**:
597
650
  ```sql
598
- SELECT * FROM rbac_organisation_roles WHERE user_id = 'your-user-id';
651
+ SELECT ror.*, u.email
652
+ FROM rbac_organisation_roles ror
653
+ JOIN auth.users u ON ror.user_id = u.id
654
+ WHERE ror.user_id = 'your-user-id'::uuid
655
+ AND ror.status = 'active';
599
656
  ```
657
+ - User must have at least one active role
658
+ - Must be for the organisation you're testing with
600
659
 
601
- 4. **Check page permissions exist**:
660
+ 6. **Check page permissions exist for user's role**:
602
661
  ```sql
603
- SELECT pp.*, ap.page_name, a.name as app_name
662
+ SELECT
663
+ ap.page_name,
664
+ pp.operation,
665
+ pp.role_name,
666
+ pp.allowed,
667
+ pp.organisation_id
604
668
  FROM rbac_page_permissions pp
605
669
  JOIN rbac_app_pages ap ON pp.app_page_id = ap.id
606
670
  JOIN rbac_apps a ON ap.app_id = a.id
607
- WHERE a.name = 'user-manager';
671
+ WHERE a.name = 'user-manager'
672
+ AND pp.role_name = 'org_admin' -- Replace with your user's role
673
+ AND pp.organisation_id = 'your-org-id'::uuid -- Replace with your org ID
674
+ AND pp.allowed = true;
608
675
  ```
676
+ - Must have permissions for the pages you're trying to access
677
+ - Must have `allowed = true` for the operation (read, create, etc.)
678
+ - `organisation_id` must match the organisation the user is accessing
679
+
680
+ 7. **Check browser console for specific errors**:
681
+ - Look for `[PagePermissionGuard]` error messages
682
+ - Look for `[useCan]` permission check logs
683
+ - Look for rate limit errors (if you see these, you may need to increase the limit)
609
684
 
610
685
  ### Issue: 400/406 Database Errors
611
686
 
@@ -12,11 +12,134 @@ This guide helps you resolve common issues when using the PACE Core RBAC system.
12
12
 
13
13
  ## 🚨 Critical Issues (Fix Immediately)
14
14
 
15
- ### 1. "Access Denied" on All Pages - Missing App ID
15
+ ### 1. "STRICT MODE VIOLATION" Error - Page Access Denied
16
+
17
+ **Symptoms:**
18
+ - Console shows `[PagePermissionGuard] STRICT MODE VIOLATION: User attempted to access protected page without permission`
19
+ - Error object shows: `{ pageName: "menus", operation: "read", userId: "...", scope: {...} }`
20
+ - Pages show fallback/access denied content even for users who should have permission
21
+ - All permission checks return `false`
22
+
23
+ **Root Cause:** One or more of these issues:
24
+ 1. Missing or incorrect database setup (app, pages, or permissions)
25
+ 2. User doesn't have organisation role for the organisation being accessed
26
+ 3. Page permissions don't exist for user's role and organisation
27
+ 4. `setupRBAC()` wasn't called before using RBAC features
28
+ 5. Rate limiting is too restrictive (unlikely with default 1000/minute)
29
+
30
+ **Fix (Check in order):**
31
+
32
+ **Step 1: Verify setupRBAC was called**
33
+ ```typescript
34
+ // Check browser console for this message:
35
+ // "RBAC system initialized successfully"
36
+
37
+ // If missing, add to main.tsx:
38
+ import { setupRBAC } from '@jmruthers/pace-core/rbac';
39
+ setupRBAC(supabase); // BEFORE rendering App
40
+ ```
41
+
42
+ **Step 2: Verify app registration**
43
+ ```sql
44
+ -- Check app exists and is active
45
+ SELECT id, name, display_name, requires_event, is_active
46
+ FROM rbac_apps
47
+ WHERE name = 'your-app-name'; -- Replace with actual app name
48
+
49
+ -- Must return 1 row with is_active = true
50
+ -- name must match VITE_APP_NAME environment variable exactly
51
+ ```
52
+
53
+ **Step 3: Verify pages exist**
54
+ ```sql
55
+ -- Check pages exist for your app
56
+ SELECT ap.id, ap.page_name, ap.page_description
57
+ FROM rbac_app_pages ap
58
+ JOIN rbac_apps a ON ap.app_id = a.id
59
+ WHERE a.name = 'your-app-name'
60
+ ORDER BY ap.page_name;
61
+
62
+ -- Must have a page matching your pageName prop (e.g., "menus")
63
+ ```
64
+
65
+ **Step 4: Verify user has organisation role**
66
+ ```sql
67
+ -- Check user's role for the organisation
68
+ SELECT ror.*, u.email
69
+ FROM rbac_organisation_roles ror
70
+ JOIN auth.users u ON ror.user_id = u.id
71
+ WHERE ror.user_id = 'user-id-from-error'::uuid
72
+ AND ror.organisation_id = 'org-id-from-scope'::uuid -- From error scope
73
+ AND ror.status = 'active';
74
+
75
+ -- User must have at least one active role (e.g., 'org_admin', 'leader', 'member')
76
+ ```
77
+
78
+ **Step 5: Verify page permissions exist**
79
+ ```sql
80
+ -- Check permissions for user's role, page, and organisation
81
+ SELECT
82
+ ap.page_name,
83
+ pp.operation,
84
+ pp.role_name,
85
+ pp.allowed,
86
+ pp.organisation_id
87
+ FROM rbac_page_permissions pp
88
+ JOIN rbac_app_pages ap ON pp.app_page_id = ap.id
89
+ JOIN rbac_apps a ON ap.app_id = a.id
90
+ WHERE a.name = 'your-app-name'
91
+ AND ap.page_name = 'menus' -- From error pageName
92
+ AND pp.operation = 'read' -- From error operation
93
+ AND pp.role_name = 'org_admin' -- From Step 4 role
94
+ AND pp.organisation_id = 'org-id-from-scope'::uuid -- From error scope
95
+ AND pp.allowed = true;
96
+
97
+ -- Must return at least 1 row with allowed = true
98
+ ```
99
+
100
+ **Step 6: Create missing permissions if needed**
101
+ ```sql
102
+ -- Grant read permission for a page to a role
103
+ INSERT INTO rbac_page_permissions (app_page_id, operation, role_name, allowed, organisation_id)
104
+ SELECT
105
+ ap.id,
106
+ 'read',
107
+ 'org_admin', -- Or user's actual role
108
+ true,
109
+ 'your-org-id'::uuid -- From error scope
110
+ FROM rbac_app_pages ap
111
+ JOIN rbac_apps a ON ap.app_id = a.id
112
+ WHERE a.name = 'your-app-name'
113
+ AND ap.page_name = 'menus';
114
+ ```
115
+
116
+ ### 2. "rate_limit_exceeded" Error
117
+
118
+ **Symptoms:**
119
+ - Console shows `[RBAC Security] Object { type: "rate_limit_exceeded", userId: "...", details: {...} }`
120
+ - Permission checks stop working temporarily
121
+ - Users can't access pages they should have permission for
122
+
123
+ **Root Cause:** Default rate limit (1000 requests/minute) exceeded
124
+
125
+ **Fix:**
126
+ ```typescript
127
+ // Increase rate limit in setupRBAC
128
+ import { setupRBAC } from '@jmruthers/pace-core/rbac';
129
+
130
+ setupRBAC(supabase, {
131
+ security: {
132
+ maxPermissionChecksPerMinute: 2000 // Increase from default 1000
133
+ }
134
+ });
135
+ ```
136
+
137
+ **Note:** Rate limiting is per user, so if you have many components checking permissions on page load, you may need a higher limit.
138
+
139
+ ### 3. "Access Denied" on All Pages - Missing App ID
16
140
 
17
141
  **Symptoms:**
18
142
  - Users see "Access Denied" on every page
19
- - Console shows `[PagePermissionGuard] STRICT MODE VIOLATION` errors
20
143
  - Console shows `scope: { organisationId: "...", eventId: "..." }` (missing `appId`)
21
144
  - All permission checks return `false`
22
145
 
@@ -16,6 +16,7 @@ PACE Core is designed with security as a first-class concern, providing:
16
16
  - **Comprehensive RBAC system** with fine-grained permissions
17
17
  - **Secure data access** with row-level security
18
18
  - **Audit logging** for compliance and monitoring
19
+ - **Automatic login history tracking** for security audits and compliance
19
20
  - **Input validation** and sanitization
20
21
  - **XSS protection** and secure coding practices
21
22
  - **Auto-logout on inactivity** for enhanced security
@@ -59,6 +60,64 @@ Sessions are automatically managed by PACE Core:
59
60
  - **Session validation** on every request
60
61
  - **Automatic logout** on token expiration
61
62
  - **Inactivity auto-logout** after 30 minutes of inactivity (configurable)
63
+ - **Automatic login history tracking** - All user logins are automatically recorded
64
+
65
+ ### 2.1 Login History Tracking
66
+
67
+ PACE Core **automatically tracks all user logins** for security auditing and compliance - no manual intervention required.
68
+
69
+ - **Fully Automatic** - Simply use `UnifiedAuthProvider` with `appName` prop - tracking happens automatically
70
+ - **No Configuration Needed** - No calls to tracking functions, no setup code, no manual intervention
71
+ - **Complete Audit Trail** - Records user ID, email, timestamp, IP address, user agent, and application context
72
+ - **Database Storage** - All login events are stored in `rbac_user_login_history` table automatically
73
+ - **Application Context** - Tracks which application the user logged into (when `appName` is provided)
74
+ - **Non-Blocking** - Tracking failures don't prevent authentication from succeeding
75
+ - **Privacy Compliant** - Users can only view their own login history (RLS enforced)
76
+
77
+ **How It Works:**
78
+
79
+ Login history tracking is **completely automatic** when you use `UnifiedAuthProvider`. Simply provide the `appName` prop (which is already required) and tracking happens automatically:
80
+
81
+ ```tsx
82
+ import { UnifiedAuthProvider } from '@jmruthers/pace-core';
83
+
84
+ function App() {
85
+ return (
86
+ <UnifiedAuthProvider
87
+ supabaseClient={supabaseClient}
88
+ appName="MY_APP" // Optional: Enables app-specific tracking
89
+ // ... other props
90
+ >
91
+ <YourApp />
92
+ </UnifiedAuthProvider>
93
+ );
94
+ }
95
+ ```
96
+
97
+ **Querying Login History:**
98
+
99
+ Login history can be queried directly from the database:
100
+
101
+ ```sql
102
+ -- Get user's login history
103
+ SELECT
104
+ login_timestamp,
105
+ email,
106
+ ip_address,
107
+ user_agent,
108
+ app_id
109
+ FROM rbac_user_login_history
110
+ WHERE user_id = auth.uid()
111
+ ORDER BY login_timestamp DESC
112
+ LIMIT 100;
113
+ ```
114
+
115
+ **Security Notes:**
116
+
117
+ - Login history insertion uses `SECURITY DEFINER` functions (bypasses RLS)
118
+ - RLS policies ensure users can only view their own login history
119
+ - Failed tracking attempts are logged but don't break authentication
120
+ - All tracking is asynchronous and non-blocking
62
121
 
63
122
  ```tsx
64
123
  import { useUnifiedAuth } from '@jmruthers/pace-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.108",
3
+ "version": "0.5.110",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -64,12 +64,12 @@ const mockUseResolvedScope = vi.hoisted(() => vi.fn(() => ({
64
64
  })));
65
65
 
66
66
  const mockUsePermissions = vi.hoisted(() => vi.fn(() => ({
67
- permissions: {} as any,
67
+ permissions: { '*': true } as any, // Grant all permissions by default (super admin access)
68
68
  isLoading: false,
69
69
  error: null,
70
- hasPermission: vi.fn(() => false),
71
- hasAnyPermission: vi.fn(() => false),
72
- hasAllPermissions: vi.fn(() => false),
70
+ hasPermission: vi.fn(() => true), // Default to allowing all permissions
71
+ hasAnyPermission: vi.fn(() => true), // Default to allowing all permissions
72
+ hasAllPermissions: vi.fn(() => true), // Default to allowing all permissions
73
73
  refetch: vi.fn(),
74
74
  })));
75
75
 
@@ -137,6 +137,40 @@ describe('NavigationMenu Component', () => {
137
137
  console.log = vi.fn();
138
138
  console.warn = vi.fn();
139
139
  console.error = vi.fn();
140
+
141
+ // Reset mocks to default permissive state (super admin access)
142
+ // Tests that need to test permission filtering can override these
143
+ mockUsePermissions.mockReturnValue({
144
+ permissions: { '*': true } as any,
145
+ isLoading: false,
146
+ error: null,
147
+ hasPermission: vi.fn(() => true),
148
+ hasAnyPermission: vi.fn(() => true),
149
+ hasAllPermissions: vi.fn(() => true),
150
+ refetch: vi.fn(),
151
+ });
152
+
153
+ mockUseRBAC.mockReturnValue({
154
+ user: { id: 'test-user', email: 'test@example.com' },
155
+ globalRole: null,
156
+ organisationRole: null,
157
+ eventAppRole: null,
158
+ hasPermission: vi.fn(),
159
+ hasGlobalPermission: vi.fn(),
160
+ isSuperAdmin: false,
161
+ isOrgAdmin: false,
162
+ isEventAdmin: false,
163
+ canManageOrganisation: false,
164
+ canManageEvent: false,
165
+ isLoading: false,
166
+ error: null,
167
+ });
168
+
169
+ mockUseResolvedScope.mockReturnValue({
170
+ resolvedScope: { organisationId: 'test-org-1', eventId: undefined, appId: undefined },
171
+ isLoading: false,
172
+ });
173
+
140
174
  // Note: hasPermission, hasRole, hasAccessLevel were removed from UnifiedAuthProvider
141
175
  // Use useRBAC() hook for permissions instead
142
176
  });