@jmruthers/pace-core 0.5.110 → 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 (230) hide show
  1. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  2. package/dist/{DataTable-D3BK2FCN.js → DataTable-5W2HVLLV.js} +8 -8
  3. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
  4. package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
  5. package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
  6. package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
  7. package/dist/chunk-2BIDKXQU.js.map +1 -0
  8. package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
  9. package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
  10. package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
  11. package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
  12. package/dist/chunk-IWJYNWXN.js.map +1 -0
  13. package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
  14. package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
  15. package/dist/chunk-MW73E7SP.js.map +1 -0
  16. package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
  17. package/dist/chunk-PXXS26G5.js.map +1 -0
  18. package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
  19. package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
  20. package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
  21. package/dist/chunk-UGVU7L7N.js.map +1 -0
  22. package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
  23. package/dist/chunk-X7SPKHYZ.js.map +1 -0
  24. package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
  25. package/dist/chunk-ZL45MG76.js.map +1 -0
  26. package/dist/components.js +10 -10
  27. package/dist/hooks.d.ts +11 -1
  28. package/dist/hooks.js +9 -7
  29. package/dist/hooks.js.map +1 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +13 -13
  32. package/dist/providers.d.ts +2 -2
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +13 -8
  35. package/dist/rbac/index.js +9 -9
  36. package/dist/utils.js +1 -1
  37. package/docs/api/classes/ColumnFactory.md +1 -1
  38. package/docs/api/classes/ErrorBoundary.md +1 -1
  39. package/docs/api/classes/InvalidScopeError.md +4 -4
  40. package/docs/api/classes/MissingUserContextError.md +4 -4
  41. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  42. package/docs/api/classes/PermissionDeniedError.md +4 -4
  43. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  44. package/docs/api/classes/RBACAuditManager.md +8 -8
  45. package/docs/api/classes/RBACCache.md +8 -8
  46. package/docs/api/classes/RBACEngine.md +4 -4
  47. package/docs/api/classes/RBACError.md +4 -4
  48. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  49. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  50. package/docs/api/classes/StorageUtils.md +1 -1
  51. package/docs/api/enums/FileCategory.md +1 -1
  52. package/docs/api/interfaces/AggregateConfig.md +1 -1
  53. package/docs/api/interfaces/ButtonProps.md +1 -1
  54. package/docs/api/interfaces/CardProps.md +1 -1
  55. package/docs/api/interfaces/ColorPalette.md +1 -1
  56. package/docs/api/interfaces/ColorShade.md +1 -1
  57. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  58. package/docs/api/interfaces/DataRecord.md +1 -1
  59. package/docs/api/interfaces/DataTableAction.md +1 -1
  60. package/docs/api/interfaces/DataTableColumn.md +1 -1
  61. package/docs/api/interfaces/DataTableProps.md +1 -1
  62. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  63. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  64. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  65. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  66. package/docs/api/interfaces/FileMetadata.md +1 -1
  67. package/docs/api/interfaces/FileReference.md +1 -1
  68. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  69. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  70. package/docs/api/interfaces/FileUploadProps.md +1 -1
  71. package/docs/api/interfaces/FooterProps.md +1 -1
  72. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  73. package/docs/api/interfaces/InputProps.md +1 -1
  74. package/docs/api/interfaces/LabelProps.md +1 -1
  75. package/docs/api/interfaces/LoginFormProps.md +1 -1
  76. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  77. package/docs/api/interfaces/NavigationContextType.md +1 -1
  78. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  79. package/docs/api/interfaces/NavigationItem.md +1 -1
  80. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  81. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  82. package/docs/api/interfaces/Organisation.md +1 -1
  83. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  84. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  85. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  86. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  87. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  88. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  89. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  90. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  91. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  92. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  93. package/docs/api/interfaces/PaletteData.md +1 -1
  94. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  95. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  96. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  98. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  99. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  102. package/docs/api/interfaces/RBACConfig.md +1 -1
  103. package/docs/api/interfaces/RBACLogger.md +1 -1
  104. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  105. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  106. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  107. package/docs/api/interfaces/RouteConfig.md +19 -6
  108. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  109. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  110. package/docs/api/interfaces/StorageConfig.md +1 -1
  111. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  112. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  113. package/docs/api/interfaces/StorageListOptions.md +1 -1
  114. package/docs/api/interfaces/StorageListResult.md +1 -1
  115. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  116. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  117. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  118. package/docs/api/interfaces/StyleImport.md +1 -1
  119. package/docs/api/interfaces/SwitchProps.md +1 -1
  120. package/docs/api/interfaces/ToastActionElement.md +1 -1
  121. package/docs/api/interfaces/ToastProps.md +1 -1
  122. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  128. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  130. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  133. package/docs/api/interfaces/UserEventAccess.md +1 -1
  134. package/docs/api/interfaces/UserMenuProps.md +1 -1
  135. package/docs/api/interfaces/UserProfile.md +1 -1
  136. package/docs/api/modules.md +36 -36
  137. package/docs/api-reference/hooks.md +8 -4
  138. package/docs/architecture/rpc-function-standards.md +3 -1
  139. package/docs/best-practices/common-patterns.md +3 -3
  140. package/docs/best-practices/deployment.md +10 -4
  141. package/docs/best-practices/performance.md +11 -3
  142. package/docs/core-concepts/organisations.md +8 -8
  143. package/docs/core-concepts/permissions.md +133 -72
  144. package/docs/migration/rbac-migration.md +65 -66
  145. package/docs/rbac/advanced-patterns.md +15 -22
  146. package/docs/rbac/examples.md +12 -12
  147. package/docs/rbac/getting-started.md +3 -3
  148. package/docs/rbac/troubleshooting.md +2 -1
  149. package/package.json +1 -1
  150. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  151. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  152. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  153. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  154. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  155. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  156. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  157. package/src/components/FileUpload/FileUpload.tsx +2 -8
  158. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  159. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  160. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  161. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  162. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  163. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  164. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  165. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  166. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  167. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  168. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  169. package/src/hooks/index.ts +1 -1
  170. package/src/hooks/useFileDisplay.ts +51 -0
  171. package/src/hooks/usePermissionCache.test.ts +112 -68
  172. package/src/hooks/usePermissionCache.ts +55 -15
  173. package/src/rbac/README.md +81 -39
  174. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  175. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  176. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  177. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  178. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  179. package/src/rbac/adapters.tsx +4 -4
  180. package/src/rbac/api.test.ts +37 -13
  181. package/src/rbac/api.ts +25 -8
  182. package/src/rbac/audit.test.ts +2 -2
  183. package/src/rbac/audit.ts +14 -5
  184. package/src/rbac/cache.test.ts +12 -0
  185. package/src/rbac/cache.ts +29 -9
  186. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  187. package/src/rbac/components/NavigationGuard.tsx +14 -14
  188. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  189. package/src/rbac/components/PagePermissionGuard.tsx +4 -3
  190. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  191. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  192. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  193. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  194. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  195. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  196. package/src/rbac/docs/event-based-apps.md +6 -6
  197. package/src/rbac/engine.ts +12 -2
  198. package/src/rbac/hooks/useCan.test.ts +29 -2
  199. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  200. package/src/rbac/hooks/usePermissions.ts +47 -23
  201. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  202. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  203. package/src/rbac/hooks/useRBAC.ts +0 -55
  204. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  205. package/src/rbac/permissions.test.ts +11 -7
  206. package/src/rbac/security.test.ts +2 -2
  207. package/src/rbac/security.ts +22 -7
  208. package/src/rbac/types.test.ts +2 -2
  209. package/src/rbac/types.ts +1 -2
  210. package/src/services/EventService.ts +41 -13
  211. package/src/services/__tests__/EventService.test.ts +25 -4
  212. package/src/services/interfaces/IEventService.ts +1 -0
  213. package/src/utils/file-reference.ts +9 -0
  214. package/dist/chunk-2W4WKJVF.js.map +0 -1
  215. package/dist/chunk-3J5N2T2N.js.map +0 -1
  216. package/dist/chunk-7GBEBJLR.js.map +0 -1
  217. package/dist/chunk-AUXS7XSO.js.map +0 -1
  218. package/dist/chunk-HADXAZT3.js.map +0 -1
  219. package/dist/chunk-Q7APDV6H.js.map +0 -1
  220. package/dist/chunk-XRSP3H52.js.map +0 -1
  221. /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-5W2HVLLV.js.map} +0 -0
  222. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
  223. /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
  224. /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
  225. /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
  226. /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
  227. /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
  228. /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
  229. /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
  230. /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
@@ -143,7 +143,7 @@ describe('RBACAuditManager', () => {
143
143
  eventId: 'event-789',
144
144
  appId: 'app-101' as UUID,
145
145
  pageId: 'page-202' as UUID,
146
- permission: 'manage:users',
146
+ permission: 'update:users',
147
147
  source: 'api' as AuditEventSource,
148
148
  metadata: { reason: 'Insufficient role' }
149
149
  };
@@ -159,7 +159,7 @@ describe('RBACAuditManager', () => {
159
159
  event_id: 'event-789',
160
160
  app_id: 'app-101',
161
161
  page_id: 'page-202',
162
- permission: 'manage:users',
162
+ permission: 'update:users',
163
163
  source: 'api',
164
164
  metadata: expect.objectContaining({ reason: 'Insufficient role' })
165
165
  })
package/src/rbac/audit.ts CHANGED
@@ -163,13 +163,22 @@ export class RBACAuditManager {
163
163
  }
164
164
 
165
165
  try {
166
- // For events without organisationId, store in a special way
166
+ // Since organisationId is now required in SecurityContext, this should rarely happen
167
+ // But we still handle the edge case properly without masking it
168
+ if (!event.organisationId) {
169
+ console.warn('[RBAC Audit] Audit event without organisation context - this should be investigated:', {
170
+ userId: event.userId,
171
+ eventType: event.type,
172
+ note: 'Organisation context is required for RBAC operations. This may indicate a security issue or missing context derivation.'
173
+ });
174
+ }
175
+
167
176
  const auditEvent: Omit<RBACAuditEvent, 'id' | 'created_at'> = {
168
177
  event_type: event.type,
169
178
  user_id: event.userId,
170
- // CRITICAL: Store organisationId even if null for auditing
171
- // Use a fallback UUID if organisation context is missing (for database constraint)
172
- organisation_id: event.organisationId || '00000000-0000-0000-0000-000000000000' as UUID,
179
+ // Store organisationId - nullable to properly track missing context cases
180
+ // Do NOT use fallback UUID as it masks security issues
181
+ organisation_id: event.organisationId || null, // Explicitly null if missing
173
182
  event_id: 'eventId' in event ? event.eventId : undefined,
174
183
  app_id: 'appId' in event ? event.appId : undefined,
175
184
  page_id: 'pageId' in event ? event.pageId : undefined,
@@ -182,7 +191,7 @@ export class RBACAuditManager {
182
191
  ...event.metadata,
183
192
  cache_hit: 'cache_hit' in event ? event.cache_hit : undefined,
184
193
  cache_source: 'cache_source' in event ? event.cache_source : undefined,
185
- // Store a flag indicating this event had no organisation context
194
+ // Explicit flag indicating this event had no organisation context
186
195
  no_organisation_context: !event.organisationId,
187
196
  },
188
197
  };
@@ -129,6 +129,18 @@ describe('RBACCache', () => {
129
129
  expect(cache.get('org-789-data')).not.toBeNull();
130
130
  });
131
131
 
132
+ it('supports wildcard patterns spanning cache segments', () => {
133
+ cache.set('perm:user-123:org-789:null:null:view:dashboard', true);
134
+ cache.set('perm:user-456:org-789:null:null:view:dashboard', true);
135
+ cache.set('perm:user-123:org-000:null:null:view:dashboard', true);
136
+
137
+ cache.invalidate('perm:*:org-789:*');
138
+
139
+ expect(cache.get('perm:user-123:org-789:null:null:view:dashboard')).toBeNull();
140
+ expect(cache.get('perm:user-456:org-789:null:null:view:dashboard')).toBeNull();
141
+ expect(cache.get('perm:user-123:org-000:null:null:view:dashboard')).toBe(true);
142
+ });
143
+
132
144
  it('handles empty pattern gracefully', () => {
133
145
  expect(() => cache.invalidate('')).not.toThrow();
134
146
  });
package/src/rbac/cache.ts CHANGED
@@ -72,18 +72,38 @@ export class RBACCache {
72
72
  * @param pattern - Pattern to match against cache keys
73
73
  */
74
74
  invalidate(pattern: string): void {
75
+ const trimmedPattern = pattern?.trim();
76
+
77
+ if (!trimmedPattern) {
78
+ return;
79
+ }
80
+
81
+ const matcher = this.createMatcher(trimmedPattern);
75
82
  const keysToDelete: string[] = [];
76
-
83
+
77
84
  for (const key of this.cache.keys()) {
78
- if (key.includes(pattern)) {
85
+ if (matcher(key)) {
79
86
  keysToDelete.push(key);
80
87
  }
81
88
  }
82
89
 
83
90
  keysToDelete.forEach(key => this.cache.delete(key));
84
-
91
+
85
92
  // Notify invalidation callbacks
86
- this.invalidationCallbacks.forEach(callback => callback(pattern));
93
+ this.invalidationCallbacks.forEach(callback => callback(trimmedPattern));
94
+ }
95
+
96
+ private createMatcher(pattern: string): (key: string) => boolean {
97
+ if (pattern.includes('*')) {
98
+ const escapedSegments = pattern
99
+ .split('*')
100
+ .map(segment => segment.replace(/[|\\{}()[\]^$+?.-]/g, '\\$&'));
101
+ const regexPattern = escapedSegments.join('.*');
102
+ const regex = new RegExp(regexPattern);
103
+ return (key: string) => regex.test(key);
104
+ }
105
+
106
+ return (key: string) => key.includes(pattern);
87
107
  }
88
108
 
89
109
  /**
@@ -239,9 +259,9 @@ export const rbacCache = new RBACCache();
239
259
  * Cache key patterns for invalidation
240
260
  */
241
261
  export const CACHE_PATTERNS = {
242
- USER: (userId: UUID) => `user:${userId}`,
243
- ORGANISATION: (organisationId: UUID) => `org:${organisationId}`,
244
- EVENT: (eventId: string) => `event:${eventId}`,
245
- APP: (appId: UUID) => `app:${appId}`,
246
- PERMISSION: (userId: UUID, organisationId: UUID) => `perm:${userId}:${organisationId}`,
262
+ USER: (userId: UUID) => `:${userId}:`,
263
+ ORGANISATION: (organisationId: UUID) => `:${organisationId}:`,
264
+ EVENT: (eventId: string) => `:${eventId}:`,
265
+ APP: (appId: UUID) => `:${appId}`,
266
+ PERMISSION: (userId: UUID, organisationId: UUID) => `perm:${userId}:${organisationId}:`,
247
267
  } as const;
@@ -74,7 +74,7 @@ const mockNavigationItems: NavigationItem[] = [
74
74
  id: 'admin',
75
75
  label: 'Admin',
76
76
  path: '/admin',
77
- permissions: ['manage:admin'] as Permission[],
77
+ permissions: ['update:admin'] as Permission[],
78
78
  pageId: 'page-admin',
79
79
  accessLevel: 'admin',
80
80
  meta: {
@@ -65,7 +65,7 @@
65
65
  */
66
66
 
67
67
  import React, { useMemo, useCallback, useEffect, useState } from 'react';
68
- import { useCan } from '../hooks';
68
+ import { useMultiplePermissions } from '../hooks/usePermissions';
69
69
  import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
70
70
  import { UUID, Permission, Scope } from '../types';
71
71
  import { createScopeFromEvent } from '../utils/eventContext';
@@ -176,26 +176,26 @@ export function NavigationGuard({
176
176
  resolveScope();
177
177
  }, [scope, selectedOrganisation, selectedEvent, supabase]);
178
178
 
179
- // Check permissions using the first permission as a representative
180
- // For multiple permissions, we'll check them sequentially
181
- const representativePermission = navigationItem.permissions[0];
182
- const { can, isLoading, error } = useCan(
179
+ // Check all permissions using useMultiplePermissions hook
180
+ const { results: permissionResults, isLoading, error } = useMultiplePermissions(
183
181
  user?.id || '',
184
182
  resolvedScope || { eventId: selectedEvent?.event_id || undefined },
185
- representativePermission,
186
- navigationItem.pageId,
183
+ navigationItem.permissions || [],
187
184
  true // Use cache
188
185
  );
189
186
 
190
- // Determine if user has required permissions
187
+ // Determine if user has required permissions based on requireAll prop
191
188
  const hasRequiredPermissions = useMemo((): boolean => {
192
- if (navigationItem.permissions.length === 0) return true;
189
+ if (!navigationItem.permissions || navigationItem.permissions.length === 0) return true;
193
190
 
194
- // For now, use the representative permission result
195
- // In a future enhancement, we could check all permissions
196
- // but this would require multiple useCan hooks or a custom hook
197
- return can;
198
- }, [navigationItem.permissions, can]);
191
+ if (requireAll) {
192
+ // User must have ALL permissions
193
+ return Object.values(permissionResults).every(result => result === true);
194
+ } else {
195
+ // User must have ANY permission (default behavior)
196
+ return Object.values(permissionResults).some(result => result === true);
197
+ }
198
+ }, [navigationItem.permissions, permissionResults, requireAll]);
199
199
 
200
200
  // Handle permission check completion
201
201
  useEffect(() => {
@@ -57,7 +57,7 @@ const mockNavigationItems: NavigationItem[] = [
57
57
  id: 'admin',
58
58
  label: 'Admin',
59
59
  path: '/admin',
60
- permissions: ['manage:admin'] as Permission[],
60
+ permissions: ['update:admin'] as Permission[],
61
61
  pageId: 'page-admin',
62
62
  accessLevel: 'admin'
63
63
  }
@@ -378,7 +378,7 @@ const PagePermissionGuardComponent = ({
378
378
  }, [operation, pageName]);
379
379
 
380
380
  // Create a stable scope that only includes valid values
381
- // This ensures useCan doesn't run with empty organisationId which causes it to stay in loading state
381
+ // OrganisationId is required - use undefined if not available, useCan will handle loading state
382
382
  const stableScope = useMemo(() => {
383
383
  if (resolvedScope && resolvedScope.organisationId) {
384
384
  return {
@@ -387,8 +387,9 @@ const PagePermissionGuardComponent = ({
387
387
  eventId: resolvedScope.eventId || undefined
388
388
  };
389
389
  }
390
- // Return a scope with empty string organisationId - useCan will handle this by keeping loading state
391
- return { organisationId: '', appId: undefined, eventId: undefined };
390
+ // Return scope without organisationId - useCan will keep loading state until resolved
391
+ // Scope.organisationId is optional, so undefined is valid
392
+ return { organisationId: undefined, appId: undefined, eventId: undefined };
392
393
  }, [resolvedScope]);
393
394
 
394
395
  // Check if user has permission - only call useCan when we have a resolved scope with valid organisationId
@@ -38,7 +38,7 @@ const mockScope: Scope = {
38
38
  appId: 'app-789' as UUID
39
39
  };
40
40
 
41
- const mockPermissions = ['read:users', 'manage:users'] as const;
41
+ const mockPermissions = ['read:users', 'update:users'] as const;
42
42
  const mockPageId = 'page-123';
43
43
 
44
44
  // Test component
@@ -19,7 +19,7 @@
19
19
  * ```tsx
20
20
  * // Basic permission enforcement
21
21
  * <PermissionEnforcer
22
- * permissions={['read:events', 'manage:events']}
22
+ * permissions={['read:events', 'update:events']}
23
23
  * operation="event-management"
24
24
  * fallback={<AccessDeniedPage />}
25
25
  * >
@@ -67,7 +67,7 @@
67
67
  */
68
68
 
69
69
  import React, { useMemo, useCallback, useEffect, useState } from 'react';
70
- import { useCan } from '../hooks';
70
+ import { useMultiplePermissions } from '../hooks/usePermissions';
71
71
  import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
72
72
  import { UUID, Permission, Scope } from '../types';
73
73
  import { createScopeFromEvent } from '../utils/eventContext';
@@ -129,7 +129,6 @@ export function PermissionEnforcer({
129
129
  const { user, selectedOrganisation, selectedEvent, supabase } = useUnifiedAuth();
130
130
  const [hasChecked, setHasChecked] = useState(false);
131
131
  const [checkError, setCheckError] = useState<Error | null>(null);
132
- const [permissionResults, setPermissionResults] = useState<Record<string, boolean>>({});
133
132
  const [resolvedScope, setResolvedScope] = useState<Scope | null>(null);
134
133
 
135
134
  // Resolve scope - either use provided scope or resolve from context
@@ -182,26 +181,31 @@ export function PermissionEnforcer({
182
181
  resolveScope();
183
182
  }, [scope, selectedOrganisation, selectedEvent, supabase]);
184
183
 
185
- // Check permissions using the first permission as a representative
186
- // For multiple permissions, we'll check them sequentially
187
- const representativePermission = permissions[0];
188
- const { can, isLoading, error } = useCan(
184
+ // Check all permissions using useMultiplePermissions hook
185
+ const { results: permissionResults, isLoading, error } = useMultiplePermissions(
189
186
  user?.id || '',
190
187
  resolvedScope || { eventId: selectedEvent?.event_id || undefined },
191
- representativePermission,
192
- undefined,
188
+ permissions,
193
189
  true // Use cache
194
190
  );
195
191
 
196
- // Determine if user has required permissions
192
+ // Determine if user has required permissions based on requireAll prop
197
193
  const hasRequiredPermissions = useMemo((): boolean => {
198
194
  if (permissions.length === 0) return true;
199
195
 
200
- // For now, use the representative permission result
201
- // In a future enhancement, we could check all permissions
202
- // but this would require multiple useCan hooks or a custom hook
203
- return can;
204
- }, [permissions, can]);
196
+ // If permissionResults is not yet available or empty, deny access
197
+ if (!permissionResults || Object.keys(permissionResults).length === 0) {
198
+ return false;
199
+ }
200
+
201
+ if (requireAll) {
202
+ // User must have ALL permissions
203
+ return Object.values(permissionResults).every(result => result === true);
204
+ } else {
205
+ // User must have ANY permission (default behavior)
206
+ return Object.values(permissionResults).some(result => result === true);
207
+ }
208
+ }, [permissions, permissionResults, requireAll]);
205
209
 
206
210
  // Handle permission check completion
207
211
  useEffect(() => {
@@ -79,6 +79,9 @@ export interface RouteConfig {
79
79
  /** Permissions required for this route */
80
80
  permissions: Permission[];
81
81
 
82
+ /** If true, this route is public and doesn't require permission checks */
83
+ public?: boolean;
84
+
82
85
  /** Roles that can access this route */
83
86
  roles?: string[];
84
87
 
@@ -232,10 +235,13 @@ export function RoleBasedRouter({
232
235
  currentRouteConfig?.pageId
233
236
  );
234
237
 
235
- // If route has no permissions, deny access (secure by default)
238
+ // Check if route is public
239
+ const isPublicRoute = currentRouteConfig?.public === true;
240
+
241
+ // If route has no permissions and is not public, deny access (secure by default)
236
242
  const hasPermissions = currentRouteConfig?.permissions && currentRouteConfig.permissions.length > 0;
237
- const finalCanAccess = hasPermissions ? canAccessCurrentRoute : false;
238
- const finalLoading = hasPermissions ? permissionLoading : false;
243
+ const finalCanAccess = isPublicRoute ? true : (hasPermissions ? canAccessCurrentRoute : false);
244
+ const finalLoading = isPublicRoute ? false : (hasPermissions ? permissionLoading : false);
239
245
 
240
246
  // Get all accessible routes for current user
241
247
  const getAccessibleRoutes = useCallback((): RouteConfig[] => {
@@ -323,13 +329,14 @@ export function RoleBasedRouter({
323
329
 
324
330
  // Use the actual permission check result
325
331
  const allowed = finalCanAccess;
332
+ // Log route access (including public routes for audit monitoring)
326
333
  recordRouteAccess(currentPath, allowed, currentRouteConfig);
327
334
 
328
- if (!allowed) {
329
- // Redirect to fallback route
335
+ if (!allowed && !isPublicRoute) {
336
+ // Redirect to fallback route (skip for public routes)
330
337
  navigate(fallbackRoute, { replace: true });
331
338
  }
332
- }, [location.pathname, currentRouteConfig, canAccessCurrentRoute, recordRouteAccess, strictMode, user?.id, currentScope, onStrictModeViolation, navigate, fallbackRoute]);
339
+ }, [location.pathname, currentRouteConfig, canAccessCurrentRoute, recordRouteAccess, strictMode, user?.id, currentScope, onStrictModeViolation, navigate, fallbackRoute, isPublicRoute]);
333
340
 
334
341
  // Context value
335
342
  const contextValue = useMemo((): RoleBasedRouterContextType => ({
@@ -350,8 +357,8 @@ export function RoleBasedRouter({
350
357
  auditLog
351
358
  ]);
352
359
 
353
- // Show loading state while checking permissions
354
- if (finalLoading) {
360
+ // Show loading state while checking permissions (skip for public routes)
361
+ if (finalLoading && !isPublicRoute) {
355
362
  return (
356
363
  <div className="flex items-center justify-center min-h-screen">
357
364
  <div className="text-center">
@@ -363,7 +370,7 @@ export function RoleBasedRouter({
363
370
  }
364
371
 
365
372
  // Show unauthorized component if user can't access current route
366
- if (currentRouteConfig && !finalCanAccess) {
373
+ if (currentRouteConfig && !finalCanAccess && !isPublicRoute) {
367
374
  return (
368
375
  <UnauthorizedComponent
369
376
  route={currentRoute}