@jmruthers/pace-core 0.5.109 → 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 (144) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/{DataTable-5HITILXS.js → DataTable-D3BK2FCN.js} +4 -4
  3. package/dist/{api-5I3E47G2.js → api-PIE4JRFS.js} +2 -2
  4. package/dist/{chunk-P72NKAT5.js → chunk-3J5N2T2N.js} +51 -11
  5. package/dist/chunk-3J5N2T2N.js.map +1 -0
  6. package/dist/{chunk-3TKTL5AZ.js → chunk-7GBEBJLR.js} +26 -34
  7. package/dist/chunk-7GBEBJLR.js.map +1 -0
  8. package/dist/{chunk-S4D3Z723.js → chunk-AWK2FAUN.js} +3 -3
  9. package/dist/{chunk-WWNOVFDC.js → chunk-HADXAZT3.js} +2 -2
  10. package/dist/{chunk-UW2DE6JX.js → chunk-HGZSO43Y.js} +2 -2
  11. package/dist/{chunk-F6TSYCKP.js → chunk-XRSP3H52.js} +12 -7
  12. package/dist/chunk-XRSP3H52.js.map +1 -0
  13. package/dist/components.js +4 -4
  14. package/dist/hooks.js +1 -1
  15. package/dist/index.js +6 -6
  16. package/dist/rbac/index.d.ts +34 -22
  17. package/dist/rbac/index.js +3 -3
  18. package/dist/utils.js +1 -1
  19. package/docs/api/classes/ColumnFactory.md +1 -1
  20. package/docs/api/classes/ErrorBoundary.md +1 -1
  21. package/docs/api/classes/InvalidScopeError.md +1 -1
  22. package/docs/api/classes/MissingUserContextError.md +1 -1
  23. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  24. package/docs/api/classes/PermissionDeniedError.md +1 -1
  25. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  26. package/docs/api/classes/RBACAuditManager.md +1 -1
  27. package/docs/api/classes/RBACCache.md +1 -1
  28. package/docs/api/classes/RBACEngine.md +9 -8
  29. package/docs/api/classes/RBACError.md +1 -1
  30. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  31. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  32. package/docs/api/classes/StorageUtils.md +1 -1
  33. package/docs/api/enums/FileCategory.md +1 -1
  34. package/docs/api/interfaces/AggregateConfig.md +1 -1
  35. package/docs/api/interfaces/ButtonProps.md +1 -1
  36. package/docs/api/interfaces/CardProps.md +1 -1
  37. package/docs/api/interfaces/ColorPalette.md +1 -1
  38. package/docs/api/interfaces/ColorShade.md +1 -1
  39. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  40. package/docs/api/interfaces/DataRecord.md +1 -1
  41. package/docs/api/interfaces/DataTableAction.md +1 -1
  42. package/docs/api/interfaces/DataTableColumn.md +1 -1
  43. package/docs/api/interfaces/DataTableProps.md +1 -1
  44. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  45. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  46. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  47. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  48. package/docs/api/interfaces/FileMetadata.md +1 -1
  49. package/docs/api/interfaces/FileReference.md +1 -1
  50. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  51. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  52. package/docs/api/interfaces/FileUploadProps.md +1 -1
  53. package/docs/api/interfaces/FooterProps.md +1 -1
  54. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  55. package/docs/api/interfaces/InputProps.md +1 -1
  56. package/docs/api/interfaces/LabelProps.md +1 -1
  57. package/docs/api/interfaces/LoginFormProps.md +1 -1
  58. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  59. package/docs/api/interfaces/NavigationContextType.md +1 -1
  60. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  61. package/docs/api/interfaces/NavigationItem.md +1 -1
  62. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  63. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  64. package/docs/api/interfaces/Organisation.md +1 -1
  65. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  66. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  67. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  68. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  69. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  70. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  71. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  72. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  73. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  74. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  75. package/docs/api/interfaces/PaletteData.md +1 -1
  76. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  77. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  78. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  79. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  80. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  81. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  82. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  83. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  84. package/docs/api/interfaces/RBACConfig.md +19 -8
  85. package/docs/api/interfaces/RBACLogger.md +5 -5
  86. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  87. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  88. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  89. package/docs/api/interfaces/RouteConfig.md +1 -1
  90. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  91. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  92. package/docs/api/interfaces/StorageConfig.md +1 -1
  93. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  94. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  95. package/docs/api/interfaces/StorageListOptions.md +1 -1
  96. package/docs/api/interfaces/StorageListResult.md +1 -1
  97. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  98. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  99. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  100. package/docs/api/interfaces/StyleImport.md +1 -1
  101. package/docs/api/interfaces/SwitchProps.md +1 -1
  102. package/docs/api/interfaces/ToastActionElement.md +1 -1
  103. package/docs/api/interfaces/ToastProps.md +1 -1
  104. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  105. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  106. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  107. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  108. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  109. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  110. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  111. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  112. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  113. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  114. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  115. package/docs/api/interfaces/UserEventAccess.md +1 -1
  116. package/docs/api/interfaces/UserMenuProps.md +1 -1
  117. package/docs/api/interfaces/UserProfile.md +1 -1
  118. package/docs/api/modules.md +21 -20
  119. package/docs/documentation-index.md +0 -2
  120. package/docs/rbac/README.md +114 -38
  121. package/docs/rbac/api-reference.md +63 -16
  122. package/docs/rbac/getting-started.md +16 -16
  123. package/docs/rbac/quick-start.md +110 -35
  124. package/docs/rbac/troubleshooting.md +125 -2
  125. package/package.json +1 -1
  126. package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
  127. package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
  128. package/src/rbac/api.test.ts +2 -2
  129. package/src/rbac/api.ts +2 -1
  130. package/src/rbac/components/PagePermissionGuard.tsx +21 -38
  131. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  132. package/src/rbac/config.ts +2 -0
  133. package/src/rbac/engine.ts +15 -5
  134. package/src/rbac/security.ts +1 -1
  135. package/dist/chunk-3TKTL5AZ.js.map +0 -1
  136. package/dist/chunk-F6TSYCKP.js.map +0 -1
  137. package/dist/chunk-P72NKAT5.js.map +0 -1
  138. package/docs/rbac/breaking-changes-v3.md +0 -222
  139. package/docs/rbac/migration-guide.md +0 -260
  140. /package/dist/{DataTable-5HITILXS.js.map → DataTable-D3BK2FCN.js.map} +0 -0
  141. /package/dist/{api-5I3E47G2.js.map → api-PIE4JRFS.js.map} +0 -0
  142. /package/dist/{chunk-S4D3Z723.js.map → chunk-AWK2FAUN.js.map} +0 -0
  143. /package/dist/{chunk-WWNOVFDC.js.map → chunk-HADXAZT3.js.map} +0 -0
  144. /package/dist/{chunk-UW2DE6JX.js.map → chunk-HGZSO43Y.js.map} +0 -0
@@ -462,10 +462,16 @@ export const NavigationMenu = React.forwardRef<
462
462
  return (items || []).filter(item => !item.meta?.hidden);
463
463
  }
464
464
 
465
- return (items || []).filter(item => {
466
- // Check if item should be hidden
467
- if (item.meta?.hidden) return false;
468
-
465
+ // Helper function to derive page ID from href
466
+ const getPageIdFromHref = (href?: string): string | null => {
467
+ if (!href) return null;
468
+ // Remove leading slash and any query params/hash
469
+ const path = href.split('?')[0].split('#')[0].replace(/^\//, '');
470
+ return path || 'home';
471
+ };
472
+
473
+ // Helper function to check if item has permission to be shown
474
+ const hasItemPermission = (item: NavigationItem): boolean => {
469
475
  // Check permissions if available
470
476
  if (item.permissions && item.permissions.length > 0) {
471
477
  // Convert string permissions to Permission type and check
@@ -547,8 +553,66 @@ export const NavigationMenu = React.forwardRef<
547
553
  }
548
554
  }
549
555
 
556
+ // NEW: Auto-check page permissions for items with href but no explicit permissions
557
+ // If item has an href but no explicit permissions/roles/accessLevel, check page permission
558
+ if (item.href && !item.permissions && !item.roles && !item.accessLevel) {
559
+ const pageId = item.pageId || getPageIdFromHref(item.href);
560
+ if (pageId) {
561
+ // Check for read permission on the page
562
+ const pagePermission: Permission = `read:page.${pageId}` as Permission;
563
+
564
+ // Check permission map (super admin has access to everything via '*' key)
565
+ const hasPagePermission = permissionMap['*'] === true || permissionMap[pagePermission] === true;
566
+
567
+ if (!hasPagePermission) {
568
+ if (auditLog) {
569
+ console.log(`[NavigationMenu] Filtering out navigation item "${item.label}" - no page permission:`, {
570
+ itemId: item.id,
571
+ href: item.href,
572
+ pageId,
573
+ permission: pagePermission,
574
+ hasPermission: hasPagePermission
575
+ });
576
+ }
577
+ return false;
578
+ }
579
+ }
580
+ }
581
+
550
582
  return true;
551
- });
583
+ };
584
+
585
+ // Helper function to filter items recursively (creates new objects to avoid mutations)
586
+ const filterItem = (item: NavigationItem): NavigationItem | null => {
587
+ // Check if item should be hidden
588
+ if (item.meta?.hidden) return null;
589
+
590
+ // Check if item has permission
591
+ if (!hasItemPermission(item)) return null;
592
+
593
+ // Recursively filter children if present
594
+ let filteredChildren: NavigationItem[] | undefined;
595
+ if (item.children && item.children.length > 0) {
596
+ filteredChildren = item.children
597
+ .map(child => filterItem(child))
598
+ .filter((child): child is NavigationItem => child !== null);
599
+
600
+ // If parent has no accessible children, hide the parent too (unless it has its own href)
601
+ if (filteredChildren.length === 0 && !item.href) {
602
+ return null;
603
+ }
604
+ }
605
+
606
+ // Return filtered item (with filtered children if applicable)
607
+ return {
608
+ ...item,
609
+ children: filteredChildren
610
+ };
611
+ };
612
+
613
+ return (items || [])
614
+ .map(item => filterItem(item))
615
+ .filter((item): item is NavigationItem => item !== null);
552
616
  }, [
553
617
  items,
554
618
  filterByPermissions,
@@ -558,7 +622,8 @@ export const NavigationMenu = React.forwardRef<
558
622
  hasAnyPermission,
559
623
  scopeLoading,
560
624
  permissionsLoading,
561
- resolvedScope
625
+ resolvedScope,
626
+ auditLog
562
627
  ]);
563
628
 
564
629
  // Log navigation access attempts for debugging
@@ -119,7 +119,7 @@ describe('RBAC API', () => {
119
119
 
120
120
  process.env.NODE_ENV = originalEnv;
121
121
 
122
- expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
122
+ expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
123
123
  expect(mockCreateAuditManager).toHaveBeenCalledWith(mockSupabase);
124
124
  expect(mockSetGlobalAuditManager).toHaveBeenCalledWith(mockAuditManager);
125
125
  expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
@@ -203,7 +203,7 @@ describe('RBAC API', () => {
203
203
 
204
204
  setupRBAC(mockSupabase as any);
205
205
 
206
- expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
206
+ expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
207
207
  });
208
208
 
209
209
  it('handles multiple initialization calls', () => {
package/src/rbac/api.ts CHANGED
@@ -49,7 +49,8 @@ export function setupRBAC(supabase: SupabaseClient<Database>, config?: Partial<R
49
49
 
50
50
  createRBACConfig(fullConfig);
51
51
 
52
- globalEngine = createRBACEngine(supabase);
52
+ // Pass security config to engine if provided
53
+ globalEngine = createRBACEngine(supabase, config?.security);
53
54
 
54
55
  // Setup audit manager
55
56
  const auditManager = createAuditManager(supabase);
@@ -148,41 +148,6 @@ const PagePermissionGuardComponent = ({
148
148
  const supabaseRef = useRef(supabase);
149
149
  supabaseRef.current = supabase;
150
150
 
151
- // Track the last scope we called useCan with to prevent infinite loops
152
- const lastScopeRef = useRef<string | null>(null);
153
-
154
- // Use a ref to store the stable scope and only update it when it actually changes
155
- const stableScopeRef = useRef<{ organisationId: string; appId: string; eventId: string | undefined }>({
156
- organisationId: '',
157
- appId: '',
158
- eventId: undefined
159
- });
160
-
161
- // Only update the stable scope if the resolved scope has actually changed
162
- if (resolvedScope && resolvedScope.organisationId) {
163
- const newScope = {
164
- organisationId: resolvedScope.organisationId,
165
- appId: resolvedScope.appId,
166
- eventId: resolvedScope.eventId
167
- };
168
-
169
- // Only update if the scope has actually changed
170
- if (stableScopeRef.current.organisationId !== newScope.organisationId ||
171
- stableScopeRef.current.eventId !== newScope.eventId ||
172
- stableScopeRef.current.appId !== newScope.appId) {
173
- stableScopeRef.current = {
174
- organisationId: newScope.organisationId,
175
- appId: newScope.appId || '',
176
- eventId: newScope.eventId
177
- };
178
- }
179
- } else if (!resolvedScope) {
180
- // Reset to empty scope when no resolved scope
181
- stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
182
- }
183
-
184
- const stableScope = stableScopeRef.current;
185
-
186
151
  // Resolve scope - either use provided scope or resolve from context
187
152
  useEffect(() => {
188
153
  const abortController = new AbortController();
@@ -412,9 +377,22 @@ const PagePermissionGuardComponent = ({
412
377
  return `${operation}:page.${pageName}` as Permission;
413
378
  }, [operation, pageName]);
414
379
 
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
382
+ const stableScope = useMemo(() => {
383
+ if (resolvedScope && resolvedScope.organisationId) {
384
+ return {
385
+ organisationId: resolvedScope.organisationId,
386
+ appId: resolvedScope.appId || undefined,
387
+ eventId: resolvedScope.eventId || undefined
388
+ };
389
+ }
390
+ // Return a scope with empty string organisationId - useCan will handle this by keeping loading state
391
+ return { organisationId: '', appId: undefined, eventId: undefined };
392
+ }, [resolvedScope]);
415
393
 
416
- // Check if user has permission - only call useCan when we have a resolved scope
417
- // If resolvedScope is null, we're still resolving, so show loading state
394
+ // Check if user has permission - only call useCan when we have a resolved scope with valid organisationId
395
+ // If resolvedScope is null or has no organisationId, useCan will keep isLoading=true
418
396
  const { can, isLoading: canIsLoading, error: canError } = useCan(
419
397
  user?.id || '',
420
398
  stableScope,
@@ -464,12 +442,17 @@ const PagePermissionGuardComponent = ({
464
442
  console.error(`[PagePermissionGuard] STRICT MODE VIOLATION: User attempted to access protected page without permission`, {
465
443
  pageName,
466
444
  operation,
445
+ permission: `${operation}:page.${pageName}`,
446
+ pageId: effectivePageId,
467
447
  userId: user?.id,
468
448
  scope: resolvedScope,
449
+ scopeValid: resolvedScope && resolvedScope.organisationId ? true : false,
450
+ checkError,
451
+ canError,
469
452
  timestamp: new Date().toISOString()
470
453
  });
471
454
  }
472
- }, [strictMode, hasChecked, isLoading, can, pageName, operation, user?.id, resolvedScope]);
455
+ }, [strictMode, hasChecked, isLoading, can, pageName, operation, effectivePageId, user?.id, resolvedScope, checkError, canError]);
473
456
 
474
457
  // Calculate the actual render state - FIXED: Proper state calculation
475
458
  // Add defensive checks to ensure we have valid state
@@ -949,7 +949,7 @@ describe('PagePermissionGuard Component', () => {
949
949
  expect.objectContaining({
950
950
  organisationId: '',
951
951
  eventId: undefined,
952
- appId: ''
952
+ appId: undefined // appId is optional and should be undefined when not resolved
953
953
  }),
954
954
  'read:page.dashboard',
955
955
  'dashboard',
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { SupabaseClient } from '@supabase/supabase-js';
11
11
  import { Database } from '../types/database';
12
+ import { RBACSecurityConfig } from './security';
12
13
 
13
14
  export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
14
15
 
@@ -26,6 +27,7 @@ export interface RBACConfig {
26
27
  enabled?: boolean;
27
28
  logLevel?: LogLevel;
28
29
  };
30
+ security?: Partial<RBACSecurityConfig>;
29
31
  }
30
32
 
31
33
  export interface RBACLogger {
@@ -36,7 +36,8 @@ import {
36
36
  RBACSecurityValidator,
37
37
  RBACSecurityMiddleware,
38
38
  SecurityContext,
39
- DEFAULT_SECURITY_CONFIG
39
+ DEFAULT_SECURITY_CONFIG,
40
+ RBACSecurityConfig
40
41
  } from './security';
41
42
 
42
43
  /**
@@ -49,9 +50,14 @@ export class RBACEngine {
49
50
  private supabase: SupabaseClient<Database>;
50
51
  private securityMiddleware: RBACSecurityMiddleware;
51
52
 
52
- constructor(supabase: SupabaseClient<Database>) {
53
+ constructor(supabase: SupabaseClient<Database>, securityConfig?: Partial<RBACSecurityConfig>) {
53
54
  this.supabase = supabase;
54
- this.securityMiddleware = new RBACSecurityMiddleware(DEFAULT_SECURITY_CONFIG);
55
+ // Merge provided security config with defaults
56
+ const mergedSecurityConfig: RBACSecurityConfig = {
57
+ ...DEFAULT_SECURITY_CONFIG,
58
+ ...securityConfig,
59
+ };
60
+ this.securityMiddleware = new RBACSecurityMiddleware(mergedSecurityConfig);
55
61
 
56
62
  // Initialize cache invalidation for automatic cache clearing
57
63
  initializeCacheInvalidation(supabase);
@@ -589,9 +595,13 @@ export class RBACEngine {
589
595
  * Create an RBAC engine instance
590
596
  *
591
597
  * @param supabase - Supabase client
598
+ * @param securityConfig - Optional security configuration
592
599
  * @returns RBACEngine instance
593
600
  */
594
- export function createRBACEngine(supabase: SupabaseClient<Database>): RBACEngine {
595
- return new RBACEngine(supabase);
601
+ export function createRBACEngine(
602
+ supabase: SupabaseClient<Database>,
603
+ securityConfig?: Partial<RBACSecurityConfig>
604
+ ): RBACEngine {
605
+ return new RBACEngine(supabase, securityConfig);
596
606
  }
597
607
 
@@ -251,7 +251,7 @@ export const DEFAULT_SECURITY_CONFIG: RBACSecurityConfig = {
251
251
  enableInputValidation: true,
252
252
  enableRateLimiting: true,
253
253
  enableAuditLogging: true,
254
- maxPermissionChecksPerMinute: 100,
254
+ maxPermissionChecksPerMinute: 1000, // Increased from 100 to 1000 for normal app usage
255
255
  suspiciousActivityThreshold: 10,
256
256
  };
257
257