@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.
- package/dist/{DataTable-6FN7XDXA.js → DataTable-3Z5HLOWF.js} +6 -6
- package/dist/{PublicLoadingSpinner-CaoRbHvJ.d.ts → PublicLoadingSpinner-CUAnTvcg.d.ts} +41 -21
- package/dist/{UnifiedAuthProvider-6C47WIML.js → UnifiedAuthProvider-CQDZRJIS.js} +3 -3
- package/dist/{chunk-QXGLU2O5.js → chunk-27MGXDD6.js} +282 -147
- package/dist/chunk-27MGXDD6.js.map +1 -0
- package/dist/{chunk-ZBLK676C.js → chunk-3CG5L6RN.js} +1 -19
- package/dist/chunk-3CG5L6RN.js.map +1 -0
- package/dist/{chunk-35ZDPMBM.js → chunk-BYXRHAIF.js} +3 -3
- package/dist/{chunk-IJOZZOGT.js → chunk-CQZU6TFE.js} +5 -5
- package/dist/{chunk-C43QIDN3.js → chunk-CTJRBUX2.js} +2 -2
- package/dist/{chunk-R4CRQUJJ.js → chunk-ENE3AB75.js} +463 -453
- package/dist/chunk-ENE3AB75.js.map +1 -0
- package/dist/{chunk-ESJTIADP.js → chunk-F64FFPOZ.js} +5 -15
- package/dist/{chunk-ESJTIADP.js.map → chunk-F64FFPOZ.js.map} +1 -1
- package/dist/{chunk-4MXVZVNS.js → chunk-TGIY2AR2.js} +2 -2
- package/dist/{chunk-XN6GWKMV.js → chunk-VZ5OR6HD.js} +161 -14
- package/dist/chunk-VZ5OR6HD.js.map +1 -0
- package/dist/{chunk-QWNJCQXZ.js → chunk-ZV77RZMU.js} +2 -2
- package/dist/{chunk-NZGLXZGP.js → chunk-ZYZCRSBD.js} +3 -54
- package/dist/chunk-ZYZCRSBD.js.map +1 -0
- package/dist/components.d.ts +1 -1
- package/dist/components.js +9 -9
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -12
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +10 -62
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +53 -28
- package/docs/api-reference/components.md +24 -0
- package/docs/api-reference/types.md +28 -0
- package/docs/architecture/rpc-function-standards.md +39 -5
- package/docs/implementation-guides/data-tables.md +55 -10
- package/docs/implementation-guides/permission-enforcement.md +4 -0
- package/docs/rbac/super-admin-guide.md +43 -5
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/DataTable/__tests__/DataTable.export.test.tsx +702 -0
- package/src/components/DataTable/components/DataTableCore.tsx +55 -36
- package/src/components/DataTable/components/ImportModal.tsx +134 -2
- package/src/components/DataTable/index.ts +3 -1
- package/src/components/DataTable/types.ts +68 -0
- package/src/components/Dialog/Dialog.tsx +0 -13
- package/src/components/FileDisplay/FileDisplay.tsx +76 -0
- package/src/components/Header/Header.tsx +5 -0
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +72 -50
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +81 -1
- package/src/components/PublicLayout/PublicPageFooter.tsx +1 -1
- package/src/components/PublicLayout/PublicPageHeader.tsx +69 -128
- package/src/components/PublicLayout/PublicPageLayout.tsx +4 -4
- package/src/components/PublicLayout/PublicPageProvider.tsx +12 -3
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +3 -18
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +3 -1
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +11 -5
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +8 -7
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +41 -46
- package/src/hooks/public/usePublicFileDisplay.ts +176 -7
- package/src/hooks/public/usePublicRouteParams.ts +0 -12
- package/src/hooks/useAppConfig.ts +15 -6
- package/src/hooks/usePermissionCache.test.ts +12 -4
- package/src/hooks/usePermissionCache.ts +3 -19
- package/src/hooks/useSecureDataAccess.ts +0 -63
- package/src/services/EventService.ts +0 -19
- package/dist/chunk-NZGLXZGP.js.map +0 -1
- package/dist/chunk-QXGLU2O5.js.map +0 -1
- package/dist/chunk-R4CRQUJJ.js.map +0 -1
- package/dist/chunk-XN6GWKMV.js.map +0 -1
- package/dist/chunk-ZBLK676C.js.map +0 -1
- /package/dist/{DataTable-6FN7XDXA.js.map → DataTable-3Z5HLOWF.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-6C47WIML.js.map → UnifiedAuthProvider-CQDZRJIS.js.map} +0 -0
- /package/dist/{chunk-35ZDPMBM.js.map → chunk-BYXRHAIF.js.map} +0 -0
- /package/dist/{chunk-IJOZZOGT.js.map → chunk-CQZU6TFE.js.map} +0 -0
- /package/dist/{chunk-C43QIDN3.js.map → chunk-CTJRBUX2.js.map} +0 -0
- /package/dist/{chunk-4MXVZVNS.js.map → chunk-TGIY2AR2.js.map} +0 -0
- /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=
|
|
241
|
+
* <UnifiedAuthProvider supabaseClient={supabase} appName={APP_NAME}>
|
|
227
242
|
* <Router>
|
|
228
243
|
* <Routes>
|
|
229
|
-
* <Route path="/login" element={<PaceLoginPage appName=
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
-
//
|
|
452
|
-
if (
|
|
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:
|
|
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
|
|
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,
|
|
21
|
-
*
|
|
20
|
+
* import { PublicPageHeader, PublicPageProvider } from '@jmruthers/pace-core';
|
|
21
|
+
*
|
|
22
|
+
* const APP_NAME = 'CORE';
|
|
23
|
+
*
|
|
22
24
|
* function PublicEventPage() {
|
|
23
25
|
* return (
|
|
24
|
-
* <
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
113
|
-
*
|
|
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
|
|
119
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
|
|
184
184
|
</main>
|
|
185
185
|
);
|
|
186
186
|
}
|