@jmruthers/pace-core 0.5.126 → 0.5.128

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 (180) hide show
  1. package/dist/{DataTable-6FN7XDXA.js → DataTable-3Z5HLOWF.js} +6 -6
  2. package/dist/{PublicLoadingSpinner-CaoRbHvJ.d.ts → PublicLoadingSpinner-CUAnTvcg.d.ts} +41 -21
  3. package/dist/{UnifiedAuthProvider-6C47WIML.js → UnifiedAuthProvider-CQDZRJIS.js} +3 -3
  4. package/dist/{chunk-QXGLU2O5.js → chunk-27MGXDD6.js} +282 -147
  5. package/dist/chunk-27MGXDD6.js.map +1 -0
  6. package/dist/{chunk-ZBLK676C.js → chunk-3CG5L6RN.js} +1 -19
  7. package/dist/chunk-3CG5L6RN.js.map +1 -0
  8. package/dist/{chunk-35ZDPMBM.js → chunk-BYXRHAIF.js} +3 -3
  9. package/dist/{chunk-IJOZZOGT.js → chunk-CQZU6TFE.js} +5 -5
  10. package/dist/{chunk-C43QIDN3.js → chunk-CTJRBUX2.js} +2 -2
  11. package/dist/{chunk-R4CRQUJJ.js → chunk-ENE3AB75.js} +463 -453
  12. package/dist/chunk-ENE3AB75.js.map +1 -0
  13. package/dist/{chunk-ESJTIADP.js → chunk-F64FFPOZ.js} +5 -15
  14. package/dist/{chunk-ESJTIADP.js.map → chunk-F64FFPOZ.js.map} +1 -1
  15. package/dist/{chunk-4MXVZVNS.js → chunk-TGIY2AR2.js} +2 -2
  16. package/dist/{chunk-XN6GWKMV.js → chunk-VZ5OR6HD.js} +161 -14
  17. package/dist/chunk-VZ5OR6HD.js.map +1 -0
  18. package/dist/{chunk-QWNJCQXZ.js → chunk-ZV77RZMU.js} +2 -2
  19. package/dist/{chunk-NZGLXZGP.js → chunk-ZYZCRSBD.js} +3 -54
  20. package/dist/chunk-ZYZCRSBD.js.map +1 -0
  21. package/dist/components.d.ts +1 -1
  22. package/dist/components.js +9 -9
  23. package/dist/hooks.js +7 -7
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +12 -12
  26. package/dist/providers.js +2 -2
  27. package/dist/rbac/index.js +7 -7
  28. package/dist/utils.d.ts +1 -1
  29. package/dist/utils.js +1 -1
  30. package/docs/api/classes/ColumnFactory.md +1 -1
  31. package/docs/api/classes/ErrorBoundary.md +1 -1
  32. package/docs/api/classes/InvalidScopeError.md +1 -1
  33. package/docs/api/classes/MissingUserContextError.md +1 -1
  34. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  35. package/docs/api/classes/PermissionDeniedError.md +1 -1
  36. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  37. package/docs/api/classes/RBACAuditManager.md +1 -1
  38. package/docs/api/classes/RBACCache.md +1 -1
  39. package/docs/api/classes/RBACEngine.md +1 -1
  40. package/docs/api/classes/RBACError.md +1 -1
  41. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  42. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  43. package/docs/api/classes/StorageUtils.md +1 -1
  44. package/docs/api/enums/FileCategory.md +1 -1
  45. package/docs/api/interfaces/AggregateConfig.md +1 -1
  46. package/docs/api/interfaces/ButtonProps.md +1 -1
  47. package/docs/api/interfaces/CardProps.md +1 -1
  48. package/docs/api/interfaces/ColorPalette.md +1 -1
  49. package/docs/api/interfaces/ColorShade.md +1 -1
  50. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  51. package/docs/api/interfaces/DataRecord.md +1 -1
  52. package/docs/api/interfaces/DataTableAction.md +1 -1
  53. package/docs/api/interfaces/DataTableColumn.md +1 -1
  54. package/docs/api/interfaces/DataTableProps.md +1 -1
  55. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  56. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  57. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  58. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  59. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  60. package/docs/api/interfaces/FileMetadata.md +1 -1
  61. package/docs/api/interfaces/FileReference.md +1 -1
  62. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  63. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  64. package/docs/api/interfaces/FileUploadProps.md +1 -1
  65. package/docs/api/interfaces/FooterProps.md +1 -1
  66. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  67. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  68. package/docs/api/interfaces/InputProps.md +1 -1
  69. package/docs/api/interfaces/LabelProps.md +1 -1
  70. package/docs/api/interfaces/LoginFormProps.md +1 -1
  71. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  72. package/docs/api/interfaces/NavigationContextType.md +1 -1
  73. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  74. package/docs/api/interfaces/NavigationItem.md +1 -1
  75. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  76. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  77. package/docs/api/interfaces/Organisation.md +1 -1
  78. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  79. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  80. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  81. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  82. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  83. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  84. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  85. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  86. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  87. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  88. package/docs/api/interfaces/PaletteData.md +1 -1
  89. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  90. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  91. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  92. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  93. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  94. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  95. package/docs/api/interfaces/PublicPageHeaderProps.md +10 -62
  96. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  97. package/docs/api/interfaces/RBACConfig.md +1 -1
  98. package/docs/api/interfaces/RBACLogger.md +1 -1
  99. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  100. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  101. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  102. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  103. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  104. package/docs/api/interfaces/RouteConfig.md +1 -1
  105. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  106. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  107. package/docs/api/interfaces/StorageConfig.md +1 -1
  108. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  109. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  110. package/docs/api/interfaces/StorageListOptions.md +1 -1
  111. package/docs/api/interfaces/StorageListResult.md +1 -1
  112. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  113. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  114. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  115. package/docs/api/interfaces/StyleImport.md +1 -1
  116. package/docs/api/interfaces/SwitchProps.md +1 -1
  117. package/docs/api/interfaces/ToastActionElement.md +1 -1
  118. package/docs/api/interfaces/ToastProps.md +1 -1
  119. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  120. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  121. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  122. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  123. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  124. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  125. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  126. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  128. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  129. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  130. package/docs/api/interfaces/UserEventAccess.md +1 -1
  131. package/docs/api/interfaces/UserMenuProps.md +1 -1
  132. package/docs/api/interfaces/UserProfile.md +1 -1
  133. package/docs/api/modules.md +53 -28
  134. package/docs/api-reference/components.md +24 -0
  135. package/docs/api-reference/types.md +28 -0
  136. package/docs/architecture/rpc-function-standards.md +39 -5
  137. package/docs/implementation-guides/data-tables.md +55 -10
  138. package/docs/implementation-guides/permission-enforcement.md +4 -0
  139. package/docs/rbac/super-admin-guide.md +43 -5
  140. package/package.json +1 -1
  141. package/src/components/Button/Button.tsx +1 -1
  142. package/src/components/DataTable/__tests__/DataTable.export.test.tsx +702 -0
  143. package/src/components/DataTable/components/DataTableCore.tsx +55 -36
  144. package/src/components/DataTable/components/ImportModal.tsx +134 -2
  145. package/src/components/DataTable/index.ts +3 -1
  146. package/src/components/DataTable/types.ts +68 -0
  147. package/src/components/Dialog/Dialog.tsx +0 -13
  148. package/src/components/FileDisplay/FileDisplay.tsx +76 -0
  149. package/src/components/Header/Header.tsx +5 -0
  150. package/src/components/PaceAppLayout/PaceAppLayout.tsx +72 -50
  151. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +81 -1
  152. package/src/components/PublicLayout/PublicPageFooter.tsx +1 -1
  153. package/src/components/PublicLayout/PublicPageHeader.tsx +69 -128
  154. package/src/components/PublicLayout/PublicPageLayout.tsx +4 -4
  155. package/src/components/PublicLayout/PublicPageProvider.tsx +12 -3
  156. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  157. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +3 -18
  158. package/src/hooks/__tests__/useAppConfig.unit.test.ts +3 -1
  159. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +11 -5
  160. package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +8 -7
  161. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +41 -46
  162. package/src/hooks/public/usePublicFileDisplay.ts +176 -7
  163. package/src/hooks/public/usePublicRouteParams.ts +0 -12
  164. package/src/hooks/useAppConfig.ts +15 -6
  165. package/src/hooks/usePermissionCache.test.ts +12 -4
  166. package/src/hooks/usePermissionCache.ts +3 -19
  167. package/src/hooks/useSecureDataAccess.ts +0 -63
  168. package/src/services/EventService.ts +0 -19
  169. package/dist/chunk-NZGLXZGP.js.map +0 -1
  170. package/dist/chunk-QXGLU2O5.js.map +0 -1
  171. package/dist/chunk-R4CRQUJJ.js.map +0 -1
  172. package/dist/chunk-XN6GWKMV.js.map +0 -1
  173. package/dist/chunk-ZBLK676C.js.map +0 -1
  174. /package/dist/{DataTable-6FN7XDXA.js.map → DataTable-3Z5HLOWF.js.map} +0 -0
  175. /package/dist/{UnifiedAuthProvider-6C47WIML.js.map → UnifiedAuthProvider-CQDZRJIS.js.map} +0 -0
  176. /package/dist/{chunk-35ZDPMBM.js.map → chunk-BYXRHAIF.js.map} +0 -0
  177. /package/dist/{chunk-IJOZZOGT.js.map → chunk-CQZU6TFE.js.map} +0 -0
  178. /package/dist/{chunk-C43QIDN3.js.map → chunk-CTJRBUX2.js.map} +0 -0
  179. /package/dist/{chunk-4MXVZVNS.js.map → chunk-TGIY2AR2.js.map} +0 -0
  180. /package/dist/{chunk-QWNJCQXZ.js.map → chunk-ZV77RZMU.js.map} +0 -0
@@ -106,6 +106,7 @@ import { useEventTheme } from '../../hooks/useEventTheme';
106
106
  import { useCan, useResolvedScope } from '../../rbac/hooks';
107
107
  import { createScopeFromEvent } from '../../rbac/utils/eventContext';
108
108
  import { getCurrentAppName } from '../../utils/appNameResolver';
109
+ import { isSuperAdmin } from '../../rbac/api';
109
110
  import type { Permission, Scope } from '../../rbac/types';
110
111
 
111
112
  // Stable empty objects to prevent infinite loops
@@ -202,6 +203,17 @@ export interface PaceAppLayoutProps {
202
203
  * Outlet to render child routes. It provides integrated authentication, navigation,
203
204
  * and user management functionality.
204
205
  *
206
+ * **Super Admin Access:** When `enforcePermissions={true}`, PaceAppLayout automatically
207
+ * checks if the user is a super admin before enforcing permissions. Super admins bypass
208
+ * all permission checks and can access any route without violations. The component extracts
209
+ * base page names from route paths (e.g., `/organisation/scouts-victoria` → `"organisation"`)
210
+ * for permission checking, which can be overridden using `pageIdMapping`.
211
+ *
212
+ * **Important:** The appName prop should use an APP_NAME constant declared in your App.tsx
213
+ * file. This ensures consistency with public pages (via PublicPageProvider) which should
214
+ * also receive the same APP_NAME constant. The logo URL is automatically constructed as
215
+ * `/${appName.toLowerCase()}_logo_wide.svg` from the public folder.
216
+ *
205
217
  * Features:
206
218
  * - React Router v6 integration with nested routing
207
219
  * - Unified authentication integration
@@ -213,23 +225,27 @@ export interface PaceAppLayoutProps {
213
225
  * - Layout-level permission enforcement
214
226
  * - Permission-based navigation filtering
215
227
  * - Automatic page permission validation
228
+ * - Super admin bypass (super admins automatically bypass all permission checks)
216
229
  *
217
230
  * @example
218
231
  * Basic React Router setup with permission enforcement (RECOMMENDED):
219
232
  * ```tsx
220
233
  * import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
221
234
  * import { UnifiedAuthProvider } from '@jmruthers/pace-core/providers';
222
- * import { PaceAppLayout, PaceLoginPage } from '@jmruthers/pace-core';
235
+ * import { PaceAppLayout, PaceLoginPage, PublicPageApp } from '@jmruthers/pace-core';
236
+ *
237
+ * const APP_NAME = 'CORE';
223
238
  *
224
239
  * function App() {
225
240
  * return (
226
- * <UnifiedAuthProvider supabaseClient={supabase} appName="My App">
241
+ * <UnifiedAuthProvider supabaseClient={supabase} appName={APP_NAME}>
227
242
  * <Router>
228
243
  * <Routes>
229
- * <Route path="/login" element={<PaceLoginPage appName="My App" />} />
244
+ * <Route path="/login" element={<PaceLoginPage appName={APP_NAME} />} />
245
+ * <Route path="/events/*" element={<PublicPageApp appName={APP_NAME} />} />
230
246
  * <Route path="/" element={
231
247
  * <PaceAppLayout
232
- * appName="My Application"
248
+ * appName={APP_NAME}
233
249
  * enforcePermissions={true}
234
250
  * defaultPermission="read"
235
251
  * />
@@ -410,9 +426,17 @@ export function PaceAppLayout({
410
426
  }, [location.pathname, routePermissions, defaultPermission]);
411
427
 
412
428
  // Get current page ID for permission checking
429
+ // Extract base page name (first path segment) instead of full route path
430
+ // Example: /organisation/scouts-victoria -> "organisation"
413
431
  const currentPageId = useMemo(() => {
414
432
  const currentPath = location.pathname;
415
- return pageIdMapping[currentPath] || currentPath.slice(1) || 'home';
433
+ // Use pageIdMapping if provided (takes precedence)
434
+ if (pageIdMapping[currentPath]) {
435
+ return pageIdMapping[currentPath];
436
+ }
437
+ // Extract first path segment (base page name)
438
+ const pathSegments = currentPath.slice(1).split('/').filter(Boolean);
439
+ return pathSegments[0] || 'home';
416
440
  }, [location.pathname, pageIdMapping]);
417
441
 
418
442
  // Build permission string in format: operation:page.pageId
@@ -424,8 +448,37 @@ export function PaceAppLayout({
424
448
  return permissionString as Permission;
425
449
  }, [enforcePermissions, currentRoutePermission, currentPageId]);
426
450
 
451
+ // Check super admin status before permission enforcement
452
+ const [isSuperAdminUser, setIsSuperAdminUser] = useState<boolean>(false);
453
+ const [isCheckingSuperAdmin, setIsCheckingSuperAdmin] = useState<boolean>(false);
454
+
455
+ useEffect(() => {
456
+ const checkSuperAdminStatus = async () => {
457
+ if (!user?.id) {
458
+ setIsSuperAdminUser(false);
459
+ setIsCheckingSuperAdmin(false);
460
+ return;
461
+ }
462
+
463
+ setIsCheckingSuperAdmin(true);
464
+ try {
465
+ const superAdminStatus = await isSuperAdmin(user.id);
466
+ setIsSuperAdminUser(superAdminStatus);
467
+ } catch (error) {
468
+ console.error('[PaceAppLayout] Error checking super admin status:', error);
469
+ setIsSuperAdminUser(false);
470
+ } finally {
471
+ setIsCheckingSuperAdmin(false);
472
+ }
473
+ };
474
+
475
+ checkSuperAdminStatus();
476
+ }, [user?.id]);
477
+
427
478
  // Use useCan hook for permission checking (standardized approach)
428
- const { can, isLoading: isCheckingPermission, error: permissionError } = useCan(
479
+ // Note: The database function already handles super admin bypass, but we check here
480
+ // as an additional safety layer to prevent unnecessary permission checks
481
+ const { can: canFromHook, isLoading: isCheckingPermission, error: permissionError } = useCan(
429
482
  user?.id || '',
430
483
  scope,
431
484
  currentPermission,
@@ -433,7 +486,9 @@ export function PaceAppLayout({
433
486
  true // useCache
434
487
  );
435
488
 
436
- // Permission enforcement state - sync from useCan
489
+ // Permission enforcement state - super admin bypasses all checks
490
+ // This ensures super admins never see permission errors even if useCan hasn't completed
491
+ const can = isSuperAdminUser ? true : canFromHook;
437
492
  const hasPermission = enforcePermissions ? can : true;
438
493
 
439
494
  // Handle permission check results with audit logging and callbacks
@@ -443,29 +498,19 @@ export function PaceAppLayout({
443
498
  }
444
499
 
445
500
  // Only proceed when permission check is complete (not loading)
446
- if (isCheckingPermission) {
501
+ // Wait for both super admin check and permission check to complete
502
+ if (isCheckingSuperAdmin || isCheckingPermission) {
447
503
  return;
448
504
  }
449
505
 
450
506
  // NEW: Phase 1 - Enhanced Security Features
451
- // Log page access attempt for audit
452
- if (auditLog) {
453
- console.log(`[PaceAppLayout] Page access attempt:`, {
454
- pageName: currentPageId,
455
- operation: currentRoutePermission,
456
- userId: user?.id,
457
- allowed: can,
458
- strictMode,
459
- timestamp: new Date().toISOString()
460
- });
461
- }
462
-
463
- // Handle strict mode violations
464
- if (strictMode && !can) {
507
+ // Handle strict mode violations - skip for super admins
508
+ if (strictMode && !isSuperAdminUser && !can) {
465
509
  console.error(`[PaceAppLayout] STRICT MODE VIOLATION: User attempted to access protected page without permission`, {
466
510
  pageName: currentPageId,
467
511
  operation: currentRoutePermission,
468
512
  userId: user?.id,
513
+ isSuperAdmin: isSuperAdminUser,
469
514
  timestamp: new Date().toISOString()
470
515
  });
471
516
 
@@ -474,11 +519,11 @@ export function PaceAppLayout({
474
519
  }
475
520
  }
476
521
 
477
- // Handle page access denied callback
478
- if (!can && onPageAccessDenied) {
522
+ // Handle page access denied callback - skip for super admins
523
+ if (!isSuperAdminUser && !can && onPageAccessDenied) {
479
524
  onPageAccessDenied(currentPageId, currentRoutePermission);
480
525
  }
481
- }, [enforcePermissions, can, isCheckingPermission, currentPageId, currentRoutePermission, user?.id, strictMode, auditLog, onPageAccessDenied, onStrictModeViolation]);
526
+ }, [enforcePermissions, can, isCheckingPermission, isCheckingSuperAdmin, isSuperAdminUser, currentPageId, currentRoutePermission, user?.id, strictMode, auditLog, onPageAccessDenied, onStrictModeViolation]);
482
527
 
483
528
  // Filter navigation items based on permissions
484
529
  // This works independently of route enforcement - navigation filtering doesn't require enforcePermissions
@@ -568,16 +613,6 @@ export function PaceAppLayout({
568
613
  // Check permission map (super admin check already handled in getPermissionMap)
569
614
  const hasAccess = permissionMap['*'] === true || permissionMap[fullPermission] === true;
570
615
 
571
- if (auditLog) {
572
- console.log(`[PaceAppLayout] Navigation filtering:`, {
573
- item: item.label,
574
- href: item.href,
575
- pageId,
576
- permission: fullPermission,
577
- hasAccess,
578
- });
579
- }
580
-
581
616
  return { item, hasAccess };
582
617
  });
583
618
 
@@ -692,19 +727,6 @@ export function PaceAppLayout({
692
727
  navigate(fallbackRoute, { replace: true });
693
728
  return;
694
729
  }
695
-
696
- // Log route access attempt for audit
697
- if (auditLog) {
698
- console.log(`[PaceAppLayout] Route access attempt:`, {
699
- route: currentPath,
700
- userId: user?.id,
701
- allowed: hasAccess,
702
- permissions: currentRoute.permissions,
703
- roles: currentRoute.roles,
704
- accessLevel: currentRoute.accessLevel,
705
- timestamp: new Date().toISOString()
706
- });
707
- }
708
730
  };
709
731
 
710
732
  checkRouteAccess();
@@ -729,8 +751,8 @@ export function PaceAppLayout({
729
751
  return result || { error: null };
730
752
  };
731
753
 
732
- // Show loading state while checking permissions
733
- if (enforcePermissions && isCheckingPermission) {
754
+ // Show loading state while checking permissions or super admin status
755
+ if (enforcePermissions && (isCheckingSuperAdmin || isCheckingPermission)) {
734
756
  return (
735
757
  <div className="flex items-center justify-center min-h-screen">
736
758
  <div className="text-center">
@@ -114,11 +114,13 @@ vi.mock('../../../hooks/useEventTheme', () => ({
114
114
  const mockIsPermitted = vi.fn().mockResolvedValue(true);
115
115
  const mockCheckPermission = vi.fn().mockResolvedValue(true);
116
116
 
117
+ const mockIsSuperAdmin = vi.fn().mockResolvedValue(false);
118
+
117
119
  vi.mock('../../../rbac/api', () => ({
118
120
  isPermitted: vi.fn().mockResolvedValue(true),
119
121
  getPermissionMap: vi.fn().mockResolvedValue({}),
120
122
  getAccessLevel: vi.fn().mockResolvedValue('viewer'),
121
- isSuperAdmin: vi.fn().mockResolvedValue(false),
123
+ isSuperAdmin: (...args: any[]) => mockIsSuperAdmin(...args),
122
124
  setupRBAC: vi.fn()
123
125
  }));
124
126
 
@@ -232,6 +234,10 @@ describe('PaceAppLayout Security', () => {
232
234
  mockIsPermitted.mockClear();
233
235
  mockIsPermitted.mockResolvedValue(true);
234
236
 
237
+ // Reset super admin mock
238
+ mockIsSuperAdmin.mockClear();
239
+ mockIsSuperAdmin.mockResolvedValue(false);
240
+
235
241
  // Reset RBAC hook mocks
236
242
  mockHasPermissionRBAC.mockClear();
237
243
  mockHasPermissionRBAC.mockResolvedValue(true);
@@ -789,6 +795,80 @@ describe('PaceAppLayout Security', () => {
789
795
  });
790
796
  });
791
797
 
798
+ it('allows super admin to bypass all permission checks', async () => {
799
+ // Mock super admin status
800
+ mockIsSuperAdmin.mockResolvedValueOnce(true);
801
+
802
+ // Mock useCan to return false (would normally deny access)
803
+ mockUseCan.mockReturnValueOnce({
804
+ can: false,
805
+ isLoading: false,
806
+ error: null,
807
+ refetch: vi.fn().mockResolvedValue(undefined),
808
+ });
809
+
810
+ render(
811
+ <TestWrapper>
812
+ <PaceAppLayout
813
+ appName="Test App"
814
+ enforcePermissions={true}
815
+ routePermissions={{
816
+ '/test-path': 'read'
817
+ }}
818
+ />
819
+ </TestWrapper>
820
+ );
821
+
822
+ await waitFor(() => {
823
+ // Super admin should bypass permission checks and see content
824
+ expect(screen.getByTestId('mock-header')).toBeInTheDocument();
825
+ expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
826
+ // Should NOT show access denied
827
+ expect(screen.queryByText('Access Denied')).not.toBeInTheDocument();
828
+ }, { timeout: 2000 });
829
+ }, { timeout: 3000 });
830
+
831
+ it('does not log strict mode violations for super admins', async () => {
832
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
833
+
834
+ // Mock super admin status
835
+ mockIsSuperAdmin.mockResolvedValueOnce(true);
836
+
837
+ // Mock useCan to return false (would normally trigger violation)
838
+ mockUseCan.mockReturnValueOnce({
839
+ can: false,
840
+ isLoading: false,
841
+ error: null,
842
+ refetch: vi.fn().mockResolvedValue(undefined),
843
+ });
844
+
845
+ render(
846
+ <TestWrapper>
847
+ <PaceAppLayout
848
+ appName="Test App"
849
+ enforcePermissions={true}
850
+ strictMode={true}
851
+ routePermissions={{
852
+ '/test-path': 'read'
853
+ }}
854
+ />
855
+ </TestWrapper>
856
+ );
857
+
858
+ await waitFor(() => {
859
+ // Wait for super admin check to complete
860
+ expect(mockIsSuperAdmin).toHaveBeenCalled();
861
+ });
862
+
863
+ // Should not log strict mode violations for super admins
864
+ const violationLogs = consoleSpy.mock.calls.filter(call =>
865
+ call[0]?.includes('STRICT MODE VIOLATION')
866
+ );
867
+ expect(violationLogs).toHaveLength(0);
868
+
869
+ consoleSpy.mockRestore();
870
+ }, { timeout: 3000 });
871
+
792
872
  it('prevents privilege escalation', async () => {
793
873
  // Test that users cannot escalate their privileges
794
874
  // Create a test wrapper with admin path for privilege escalation test
@@ -90,7 +90,7 @@ export function PublicPageFooter({
90
90
  const copyrightText = copyright || `© Copyright 2022–${year} all rights reserved, ${companyName}.`;
91
91
 
92
92
  return (
93
- <footer className={cn('mt-8 py-6 flex justify-center border-t border-border bg-main-100', className)}>
93
+ <footer className={cn('mt-8 py-6 flex justify-center', className)}>
94
94
  <section className='px-4 w-[min(var(--app-width),100%)] mx-auto text-center'>
95
95
  {logo && (
96
96
  <img src={logo} alt="Logo" className="h-8 w-auto" />
@@ -17,40 +17,24 @@
17
17
  *
18
18
  * @example
19
19
  * ```tsx
20
- * import { PublicPageHeader, FileDisplay } from '@jmruthers/pace-core';
21
- *
20
+ * import { PublicPageHeader, PublicPageProvider } from '@jmruthers/pace-core';
21
+ *
22
+ * const APP_NAME = 'CORE';
23
+ *
22
24
  * function PublicEventPage() {
23
25
  * return (
24
- * <PublicPageHeader
25
- * event={event}
26
- * title="Event Details"
27
- * description="Public information about this event"
28
- * showEventLogo={true}
29
- * />
26
+ * <PublicPageProvider appName={APP_NAME}>
27
+ * <PublicPageHeader
28
+ * event={event}
29
+ * title="Event Details"
30
+ * description="Public information about this event"
31
+ * showEventLogo={true}
32
+ * />
33
+ * </PublicPageProvider>
30
34
  * );
31
35
  * }
32
36
  * ```
33
37
  *
34
- * @example
35
- * ```tsx
36
- * // Using custom logo URL
37
- * <PublicPageHeader
38
- * event={event}
39
- * logoUrl="/custom-logo.svg"
40
- * logoAlt="My Custom Logo"
41
- * logoHref="/"
42
- * />
43
- * ```
44
- *
45
- * @example
46
- * ```tsx
47
- * // Using custom logo component
48
- * <PublicPageHeader
49
- * event={event}
50
- * customAppLogo={<CustomLogoComponent />}
51
- * logoHref="/dashboard"
52
- * />
53
- * ```
54
38
  *
55
39
  * @accessibility
56
40
  * - WCAG 2.1 AA compliant
@@ -67,7 +51,6 @@
67
51
  */
68
52
 
69
53
  import React, { ReactNode } from 'react';
70
- import { Link } from 'react-router-dom';
71
54
  import type { Event } from '../../types/unified';
72
55
  import { FileDisplay } from '../FileDisplay/FileDisplay';
73
56
  import { FileCategory } from '../../types/file-reference';
@@ -91,16 +74,8 @@ export interface PublicPageHeaderProps {
91
74
  className?: string;
92
75
  /** Custom content to display in the header */
93
76
  children?: ReactNode;
94
- /** Custom app logo component (overrides logoUrl and auto-generated path) */
95
- customAppLogo?: ReactNode;
96
77
  /** Custom event logo component */
97
78
  customEventLogo?: ReactNode;
98
- /** URL to the app logo image (overrides auto-generated path, but customAppLogo takes precedence) */
99
- logoUrl?: string;
100
- /** Alt text for the app logo (defaults to appName from useAppConfig) */
101
- logoAlt?: string;
102
- /** URL to navigate to when app logo is clicked */
103
- logoHref?: string;
104
79
  }
105
80
 
106
81
  /**
@@ -109,14 +84,18 @@ export interface PublicPageHeaderProps {
109
84
  * This component displays the app logo, event logo, and event information
110
85
  * in a clean, accessible layout suitable for public pages.
111
86
  *
112
- * Logo handling follows a priority order:
113
- * 1. customAppLogo prop (if provided) - highest priority
114
- * 2. logoUrl prop (if provided) - direct URL override
115
- * 3. Auto-generated path from appName via useAppConfig() - convention-based
116
- * 4. Default SVG fallback - lowest priority
87
+ * The app logo is automatically generated from the appName using the pattern:
88
+ * `/{appName.toLowerCase()}_logo_wide.svg` from the public folder.
117
89
  *
118
- * The logo can be made clickable by providing the logoHref prop, which will
119
- * wrap the logo in a Link component for navigation.
90
+ * **Important:** The appName is obtained from PublicPageProvider context, which should
91
+ * receive the APP_NAME constant from your App.tsx file. This ensures consistency with
92
+ * authenticated pages that use the same APP_NAME constant.
93
+ *
94
+ * **Event Logo Requirements:**
95
+ * - Event logo files must be marked as `is_public = true` in the `file_references` table
96
+ * - The RPC function `data_file_reference_by_category_list` must be accessible to the anonymous/public role
97
+ * - Storage bucket must allow public read access for logo files
98
+ * - If no public logo is available, a fallback UI with event initials will be displayed
120
99
  *
121
100
  * @param props - Header configuration and content
122
101
  * @returns React element with public page header
@@ -130,83 +109,26 @@ export function PublicPageHeader({
130
109
  showAppLogo = true,
131
110
  className = '',
132
111
  children,
133
- customAppLogo,
134
- customEventLogo,
135
- logoUrl,
136
- logoAlt,
137
- logoHref
112
+ customEventLogo
138
113
  }: PublicPageHeaderProps) {
139
114
  const { appName } = useAppConfig();
140
115
 
141
116
  return (
142
117
  <header className={cn(
143
- " px-4 w-[min(var(--app-width),100%)] mx-auto bg-background border-b border-sec-200 grid grid-cols-[auto_1fr_auto] place-items-center gap-2",
118
+
119
+ "w-full px-[max(0rem,calc((100vw-var(--app-width))/2-0.5rem))] grid grid-cols-[auto_1fr_auto] place-items-center gap-2",
144
120
  className
145
121
  )}>
146
122
 
147
123
  {/* Top row with logos */}
148
124
 
149
125
  {/* App Logo */}
150
- {showAppLogo && (
151
- <>
152
- {customAppLogo ? (
153
- logoHref ? (
154
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
155
- {customAppLogo}
156
- </Link>
157
- ) : (
158
- customAppLogo
159
- )
160
- ) : logoUrl ? (
161
- logoHref ? (
162
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
163
- <img
164
- className="max-w-36 object-contain row-span-2"
165
- src={logoUrl}
166
- alt={logoAlt || appName}
167
- />
168
- </Link>
169
- ) : (
170
- <img
171
- className="max-w-36 object-contain row-span-2"
172
- src={logoUrl}
173
- alt={logoAlt || appName}
174
- />
175
- )
176
- ) : appName ? (
177
- logoHref ? (
178
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
179
- <img
180
- className="max-w-36 object-contain row-span-2"
181
- src={`/${appName.toLowerCase()}_logo_wide.svg`}
182
- alt={logoAlt || appName}
183
- />
184
- </Link>
185
- ) : (
186
- <img
187
- className="max-w-36 object-contain row-span-2"
188
- src={`/${appName.toLowerCase()}_logo_wide.svg`}
189
- alt={logoAlt || appName}
190
- />
191
- )
192
- ) : (
193
- logoHref ? (
194
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
195
- <img
196
- src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
197
- alt={logoAlt || 'Logo'}
198
- className="max-w-36 object-contain row-span-2"
199
- />
200
- </Link>
201
- ) : (
202
- <img
203
- src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
204
- alt={logoAlt || 'Logo'}
205
- className="max-w-36 object-contain row-span-2"
206
- />
207
- )
208
- )}
209
- </>
126
+ {showAppLogo && appName && (
127
+ <img
128
+ className="ml-4 max-w-36 object-contain row-span-2"
129
+ src={`/${appName.toLowerCase()}_logo_wide.svg`}
130
+ alt={appName}
131
+ />
210
132
  )}
211
133
 
212
134
 
@@ -222,24 +144,43 @@ export function PublicPageHeader({
222
144
  {showEventLogo && event && (
223
145
  <>
224
146
  {customEventLogo || (
225
- <FileDisplay
226
- table_name="event"
227
- record_id={event.event_id}
228
- organisation_id={event.organisation_id}
229
- category={FileCategory.EVENT_LOGOS}
230
- displayOnly={true}
231
- showFallback={true}
232
- fallbackSize="md"
233
- className="max-w-36 row-span-2"
234
- generateFallbackText={(fileName) => {
235
- if (!event.event_name) return 'EV';
236
- return event.event_name
237
- .split(/[\s\-_]+/)
238
- .map(word => word.charAt(0).toUpperCase())
239
- .join('')
240
- .substring(0, 3);
241
- }}
242
- />
147
+ <>
148
+ {/* Log organisation_id derivation chain for debugging */}
149
+ {(() => {
150
+ console.log('[PublicPageHeader] Organisation ID Derivation Chain:', {
151
+ eventCode: eventCode,
152
+ eventId: event.event_id,
153
+ eventName: event.event_name,
154
+ organisationId: event.organisation_id,
155
+ organisationIdType: typeof event.organisation_id,
156
+ organisationIdValid: !!event.organisation_id && event.organisation_id !== '',
157
+ derivation: 'URL → eventCode → usePublicEvent → event.organisation_id FileDisplay',
158
+ note: 'Organisation ID is derived from event data fetched using event code from URL'
159
+ });
160
+ return null;
161
+ })()}
162
+ <FileDisplay
163
+ table_name="event"
164
+ record_id={event.event_id}
165
+ organisation_id={event.organisation_id}
166
+ category={FileCategory.EVENT_LOGOS}
167
+ displayOnly={true}
168
+ showFallback={true}
169
+ fallbackSize="md"
170
+ className="mr-4 max-w-36 row-span-2"
171
+ generateFallbackText={(fileName) => {
172
+ if (!event.event_name) return 'EV';
173
+ return event.event_name
174
+ .split(/[\s\-_]+/)
175
+ .map(word => word.charAt(0).toUpperCase())
176
+ .join('')
177
+ .substring(0, 3);
178
+ }}
179
+ // Note: FileDisplay automatically detects public page context
180
+ // and uses FileDisplayPublic component which queries only public files (is_public = true)
181
+ // If no public logo is found, fallback UI with event initials will be displayed
182
+ />
183
+ </>
243
184
  )}
244
185
  </>
245
186
  )}
@@ -159,13 +159,13 @@ export function PublicPageLayout({
159
159
  }
160
160
  return (
161
161
  <main className="flex flex-col items-center justify-center px-4 w-[min(var(--app-width),100%)] mx-auto py-8">
162
- <div className="text-center">
162
+
163
163
  <h1>Event Not Found</h1>
164
164
  <p>
165
165
  The event code "{eventCode}" is invalid or the event is not available for public viewing.
166
166
  </p>
167
167
  <Button onClick={handleRefetch}>Try Again</Button>
168
- </div>
168
+
169
169
  </main>
170
170
  );
171
171
  }
@@ -174,13 +174,13 @@ export function PublicPageLayout({
174
174
  if (!event && showValidationErrors) {
175
175
  return (
176
176
  <main className="flex flex-col items-center justify-center px-4 w-[min(var(--app-width),100%)] mx-auto py-8">
177
- <div className="text-center">
177
+
178
178
  <h1>Event Not Available</h1>
179
179
  <p>
180
180
  This event is not available for public viewing.
181
181
  </p>
182
182
  {handleRefetch && <Button onClick={handleRefetch}>Try Again</Button>}
183
- </div>
183
+
184
184
  </main>
185
185
  );
186
186
  }