@jmruthers/pace-core 0.6.2 → 0.6.4
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/CHANGELOG.md +45 -0
- package/cursor-rules/00-pace-core-compliance.mdc +34 -2
- package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
- package/dist/{DataTable-TPTKCX4D.js → DataTable-E7YQZD7D.js} +9 -8
- package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
- package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
- package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-QPXO24B4.js} +5 -4
- package/dist/{api-MVVQZLJI.js → api-6LVZTHDS.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/chunk-36LVWXB2.js +227 -0
- package/dist/chunk-36LVWXB2.js.map +1 -0
- package/dist/{chunk-24UVZUZG.js → chunk-3LPHPB62.js} +129 -387
- package/dist/chunk-3LPHPB62.js.map +1 -0
- package/dist/{chunk-2UOI2FG5.js → chunk-5EC5MEWX.js} +4 -4
- package/dist/{chunk-3XC4CPTD.js → chunk-7JPAB3T5.js} +244 -5727
- package/dist/chunk-7JPAB3T5.js.map +1 -0
- package/dist/{chunk-6J4GEEJR.js → chunk-ATKZM7RX.js} +53 -27
- package/dist/chunk-ATKZM7RX.js.map +1 -0
- package/dist/{chunk-EHMR7VYL.js → chunk-AVMLPIM7.js} +443 -189
- package/dist/chunk-AVMLPIM7.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-NECFR5MM.js → chunk-I6DAQMWX.js} +575 -647
- package/dist/chunk-I6DAQMWX.js.map +1 -0
- package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
- package/dist/{chunk-XWQCNGTQ.js → chunk-NN6WWZ5U.js} +173 -79
- package/dist/chunk-NN6WWZ5U.js.map +1 -0
- package/dist/{chunk-MMZ7JXPU.js → chunk-OEWDTMG7.js} +13 -21
- package/dist/{chunk-MMZ7JXPU.js.map → chunk-OEWDTMG7.js.map} +1 -1
- package/dist/{chunk-SFZUDBL5.js → chunk-YKRAFF5K.js} +70 -56
- package/dist/chunk-YKRAFF5K.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +12 -13
- package/dist/contextValidator-OOPCLPZW.js +9 -0
- package/dist/contextValidator-OOPCLPZW.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +67 -27
- package/dist/rbac/index.js +15 -8
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
- package/dist/utils.js +5 -7
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +14 -16
- package/docs/api/modules.md +3796 -2513
- package/docs/components/context-selector.md +126 -0
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- package/docs/rbac/secure-client-protection.md +330 -0
- package/package.json +10 -5
- package/scripts/audit/core/checks/compliance.cjs +72 -0
- package/scripts/audit/core/checks/dependencies.cjs +568 -28
- package/scripts/audit/core/checks/documentation.cjs +68 -3
- package/scripts/audit/core/checks/environment.cjs +2 -14
- package/scripts/audit/core/checks/error-handling.cjs +47 -6
- package/src/components/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/components/RowComponent.tsx +19 -19
- package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
- package/src/components/Dialog/Dialog.tsx +29 -1
- package/src/components/FileDisplay/FileDisplay.tsx +42 -10
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +44 -45
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +157 -36
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +2 -2
- package/src/components/index.ts +5 -5
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/useAppConfig.ts +15 -30
- package/src/hooks/useFileDisplay.ts +77 -50
- package/src/index.ts +4 -5
- package/src/providers/services/AuthServiceProvider.tsx +17 -7
- package/src/providers/services/EventServiceProvider.tsx +33 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/adapters.tsx +2 -2
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +178 -132
- package/src/rbac/components/PagePermissionGuard.tsx +38 -10
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
- package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
- package/src/rbac/hooks/permissions/useCan.ts +41 -11
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +0 -9
- package/src/rbac/hooks/useRBAC.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.ts +36 -37
- package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
- package/src/rbac/hooks/useResolvedScope.ts +35 -40
- package/src/rbac/hooks/useSecureSupabase.ts +7 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +103 -16
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +1 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
- package/src/rbac/utils/clientSecurity.ts +93 -0
- package/src/rbac/utils/contextValidator.ts +77 -168
- package/src/services/AuthService.ts +39 -7
- package/src/services/EventService.ts +285 -56
- package/src/services/OrganisationService.ts +81 -14
- package/src/services/__tests__/EventService.test.ts +1 -2
- package/src/services/base/BaseService.ts +3 -0
- package/src/utils/dynamic/dynamicUtils.ts +7 -4
- package/dist/chunk-24UVZUZG.js.map +0 -1
- package/dist/chunk-3XC4CPTD.js.map +0 -1
- package/dist/chunk-6J4GEEJR.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-EHMR7VYL.js.map +0 -1
- package/dist/chunk-NECFR5MM.js.map +0 -1
- package/dist/chunk-SFZUDBL5.js.map +0 -1
- package/dist/chunk-XWQCNGTQ.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/InvalidScopeError.md +0 -73
- package/docs/api/classes/Logger.md +0 -178
- package/docs/api/classes/MissingUserContextError.md +0 -66
- package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
- package/docs/api/classes/PermissionDeniedError.md +0 -73
- package/docs/api/classes/RBACAuditManager.md +0 -297
- package/docs/api/classes/RBACCache.md +0 -322
- package/docs/api/classes/RBACEngine.md +0 -171
- package/docs/api/classes/RBACError.md +0 -76
- package/docs/api/classes/RBACNotInitializedError.md +0 -66
- package/docs/api/classes/SecureSupabaseClient.md +0 -163
- package/docs/api/classes/StorageUtils.md +0 -328
- package/docs/api/enums/FileCategory.md +0 -184
- package/docs/api/enums/LogLevel.md +0 -54
- package/docs/api/enums/RBACErrorCode.md +0 -228
- package/docs/api/enums/RPCFunction.md +0 -118
- package/docs/api/interfaces/AddressFieldProps.md +0 -241
- package/docs/api/interfaces/AddressFieldRef.md +0 -94
- package/docs/api/interfaces/AggregateConfig.md +0 -43
- package/docs/api/interfaces/AutocompleteOptions.md +0 -75
- package/docs/api/interfaces/AvatarProps.md +0 -128
- package/docs/api/interfaces/BadgeProps.md +0 -34
- package/docs/api/interfaces/ButtonProps.md +0 -56
- package/docs/api/interfaces/CalendarProps.md +0 -73
- package/docs/api/interfaces/CardProps.md +0 -69
- package/docs/api/interfaces/ColorPalette.md +0 -7
- package/docs/api/interfaces/ColorShade.md +0 -66
- package/docs/api/interfaces/ComplianceResult.md +0 -30
- package/docs/api/interfaces/DataAccessRecord.md +0 -96
- package/docs/api/interfaces/DataRecord.md +0 -11
- package/docs/api/interfaces/DataTableAction.md +0 -252
- package/docs/api/interfaces/DataTableColumn.md +0 -504
- package/docs/api/interfaces/DataTableProps.md +0 -625
- package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
- package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
- package/docs/api/interfaces/DatabaseIssue.md +0 -41
- package/docs/api/interfaces/EmptyStateConfig.md +0 -61
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
- package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
- package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
- package/docs/api/interfaces/EventAppRoleData.md +0 -71
- package/docs/api/interfaces/ExportColumn.md +0 -90
- package/docs/api/interfaces/ExportOptions.md +0 -126
- package/docs/api/interfaces/FileDisplayProps.md +0 -249
- package/docs/api/interfaces/FileMetadata.md +0 -129
- package/docs/api/interfaces/FileReference.md +0 -118
- package/docs/api/interfaces/FileSizeLimits.md +0 -7
- package/docs/api/interfaces/FileUploadOptions.md +0 -139
- package/docs/api/interfaces/FileUploadProps.md +0 -296
- package/docs/api/interfaces/FooterProps.md +0 -107
- package/docs/api/interfaces/FormFieldProps.md +0 -166
- package/docs/api/interfaces/FormProps.md +0 -113
- package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
- package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
- package/docs/api/interfaces/InputProps.md +0 -56
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -187
- package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
- package/docs/api/interfaces/NavigationContextType.md +0 -164
- package/docs/api/interfaces/NavigationGuardProps.md +0 -139
- package/docs/api/interfaces/NavigationItem.md +0 -120
- package/docs/api/interfaces/NavigationMenuProps.md +0 -221
- package/docs/api/interfaces/NavigationProviderProps.md +0 -117
- package/docs/api/interfaces/Organisation.md +0 -140
- package/docs/api/interfaces/OrganisationContextType.md +0 -388
- package/docs/api/interfaces/OrganisationMembership.md +0 -140
- package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
- package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
- package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
- package/docs/api/interfaces/PageAccessRecord.md +0 -85
- package/docs/api/interfaces/PagePermissionContextType.md +0 -140
- package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
- package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
- package/docs/api/interfaces/PaletteData.md +0 -41
- package/docs/api/interfaces/ParsedAddress.md +0 -120
- package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
- package/docs/api/interfaces/ProgressProps.md +0 -42
- package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
- package/docs/api/interfaces/QuickFix.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
- package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
- package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
- package/docs/api/interfaces/RBACConfig.md +0 -133
- package/docs/api/interfaces/RBACContext.md +0 -52
- package/docs/api/interfaces/RBACLogger.md +0 -112
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
- package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
- package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
- package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
- package/docs/api/interfaces/RBACResult.md +0 -58
- package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
- package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
- package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
- package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
- package/docs/api/interfaces/RBACRolesListParams.md +0 -52
- package/docs/api/interfaces/RBACRolesListResult.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
- package/docs/api/interfaces/ResourcePermissions.md +0 -155
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
- package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
- package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
- package/docs/api/interfaces/RoleManagementResult.md +0 -52
- package/docs/api/interfaces/RouteAccessRecord.md +0 -107
- package/docs/api/interfaces/RouteConfig.md +0 -134
- package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
- package/docs/api/interfaces/SecureDataContextType.md +0 -168
- package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
- package/docs/api/interfaces/SetupIssue.md +0 -41
- package/docs/api/interfaces/StorageConfig.md +0 -41
- package/docs/api/interfaces/StorageFileInfo.md +0 -74
- package/docs/api/interfaces/StorageFileMetadata.md +0 -151
- package/docs/api/interfaces/StorageListOptions.md +0 -99
- package/docs/api/interfaces/StorageListResult.md +0 -41
- package/docs/api/interfaces/StorageUploadOptions.md +0 -101
- package/docs/api/interfaces/StorageUploadResult.md +0 -63
- package/docs/api/interfaces/StorageUrlOptions.md +0 -60
- package/docs/api/interfaces/StyleImport.md +0 -19
- package/docs/api/interfaces/SwitchProps.md +0 -34
- package/docs/api/interfaces/TabsContentProps.md +0 -9
- package/docs/api/interfaces/TabsListProps.md +0 -9
- package/docs/api/interfaces/TabsProps.md +0 -9
- package/docs/api/interfaces/TabsTriggerProps.md +0 -50
- package/docs/api/interfaces/TextareaProps.md +0 -53
- package/docs/api/interfaces/ToastActionElement.md +0 -12
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
- package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
- package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
- package/docs/api/interfaces/UserEventAccess.md +0 -121
- package/docs/api/interfaces/UserMenuProps.md +0 -88
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -423
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
- package/src/components/OrganisationSelector/index.ts +0 -9
- /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-E7YQZD7D.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-QPXO24B4.js.map} +0 -0
- /package/dist/{api-MVVQZLJI.js.map → api-6LVZTHDS.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-2UOI2FG5.js.map → chunk-5EC5MEWX.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
|
@@ -205,14 +205,14 @@ vi.mock('../Header', () => ({
|
|
|
205
205
|
onChangePassword,
|
|
206
206
|
currentPath,
|
|
207
207
|
onNavigate,
|
|
208
|
-
|
|
208
|
+
showContextSelector,
|
|
209
209
|
showUserMenu,
|
|
210
210
|
className
|
|
211
211
|
}: any) => (
|
|
212
212
|
<header
|
|
213
213
|
data-testid="header"
|
|
214
214
|
className={className}
|
|
215
|
-
data-show-
|
|
215
|
+
data-show-context-selector={showContextSelector !== false ? 'true' : 'false'}
|
|
216
216
|
data-show-user-menu={showUserMenu !== false ? 'true' : 'false'}
|
|
217
217
|
>
|
|
218
218
|
<div data-testid="header-logo" data-logo-url={logoUrl} data-logo-alt={logoAlt}>
|
|
@@ -421,29 +421,29 @@ describe('PaceAppLayout Component', () => {
|
|
|
421
421
|
{ withRouter: false }
|
|
422
422
|
);
|
|
423
423
|
|
|
424
|
-
expect(screen.getByTestId('header')).toHaveAttribute('data-show-
|
|
424
|
+
expect(screen.getByTestId('header')).toHaveAttribute('data-show-context-selector', 'true');
|
|
425
425
|
});
|
|
426
426
|
|
|
427
|
-
it('hides
|
|
427
|
+
it('hides context selector when showContextSelector is false', () => {
|
|
428
428
|
renderWithProviders(
|
|
429
429
|
<TestWrapper>
|
|
430
|
-
<PaceAppLayout {...baseProps}
|
|
430
|
+
<PaceAppLayout {...baseProps} showContextSelector={false} />
|
|
431
431
|
</TestWrapper>,
|
|
432
432
|
{ withRouter: false }
|
|
433
433
|
);
|
|
434
434
|
|
|
435
|
-
expect(screen.getByTestId('header')).toHaveAttribute('data-show-
|
|
435
|
+
expect(screen.getByTestId('header')).toHaveAttribute('data-show-context-selector', 'false');
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
-
it('shows
|
|
438
|
+
it('shows context selector when showContextSelector is explicitly true', () => {
|
|
439
439
|
renderWithProviders(
|
|
440
440
|
<TestWrapper>
|
|
441
|
-
<PaceAppLayout {...baseProps}
|
|
441
|
+
<PaceAppLayout {...baseProps} showContextSelector={true} />
|
|
442
442
|
</TestWrapper>,
|
|
443
443
|
{ withRouter: false }
|
|
444
444
|
);
|
|
445
445
|
|
|
446
|
-
expect(screen.getByTestId('header')).toHaveAttribute('data-show-
|
|
446
|
+
expect(screen.getByTestId('header')).toHaveAttribute('data-show-context-selector', 'true');
|
|
447
447
|
});
|
|
448
448
|
});
|
|
449
449
|
|
|
@@ -109,6 +109,7 @@ import { getCurrentAppName } from '../../utils/app/appNameResolver';
|
|
|
109
109
|
import { isSuperAdmin as checkSuperAdminApi } from '../../rbac/api';
|
|
110
110
|
import { logger } from '../../utils/core/logger';
|
|
111
111
|
import type { Permission, Scope } from '../../rbac/types';
|
|
112
|
+
import { EventContextRequiredError, OrganisationContextRequiredError } from '../../rbac/types';
|
|
112
113
|
|
|
113
114
|
// Stable empty objects to prevent infinite loops
|
|
114
115
|
const EMPTY_PAGE_ID_MAPPING = {};
|
|
@@ -130,10 +131,12 @@ export interface PaceAppLayoutProps {
|
|
|
130
131
|
appName: string;
|
|
131
132
|
/** Optional navigation items for the header menu. If not provided, uses default navigation. */
|
|
132
133
|
navItems?: NavigationItem[];
|
|
133
|
-
/** Show/hide
|
|
134
|
-
|
|
135
|
-
/** Show
|
|
136
|
-
|
|
134
|
+
/** Show/hide unified context selector (shows all accessible orgs and events) - default: true */
|
|
135
|
+
showContextSelector?: boolean;
|
|
136
|
+
/** Show organisations in context selector - default: true */
|
|
137
|
+
showOrganisations?: boolean;
|
|
138
|
+
/** Show events in context selector - default: true */
|
|
139
|
+
showEvents?: boolean;
|
|
137
140
|
/** Custom actions to display in the header (between event selector and user menu) */
|
|
138
141
|
headerActions?: React.ReactNode;
|
|
139
142
|
/** Custom logo component (overrides default logo) */
|
|
@@ -349,8 +352,9 @@ export interface PaceAppLayoutProps {
|
|
|
349
352
|
export function PaceAppLayout({
|
|
350
353
|
appName,
|
|
351
354
|
navItems,
|
|
352
|
-
|
|
353
|
-
|
|
355
|
+
showContextSelector = true,
|
|
356
|
+
showOrganisations = true,
|
|
357
|
+
showEvents = true,
|
|
354
358
|
headerActions,
|
|
355
359
|
customLogo,
|
|
356
360
|
logoHref = '/dashboard',
|
|
@@ -545,6 +549,21 @@ export function PaceAppLayout({
|
|
|
545
549
|
// Use combined super admin check (RBAC + direct check)
|
|
546
550
|
const can = isSuperAdmin ? true : canFromHook;
|
|
547
551
|
const hasPermission = enforcePermissions ? can : true;
|
|
552
|
+
|
|
553
|
+
// Check if the permission error is a context error (missing event/org context)
|
|
554
|
+
// Context errors should allow the page to render so it can show helpful messaging
|
|
555
|
+
// Real permission denials should still show "Access Denied"
|
|
556
|
+
const isContextError = useMemo(() => {
|
|
557
|
+
if (!permissionError) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
return (
|
|
561
|
+
permissionError instanceof EventContextRequiredError ||
|
|
562
|
+
permissionError instanceof OrganisationContextRequiredError ||
|
|
563
|
+
permissionError.name === 'EventContextRequiredError' ||
|
|
564
|
+
permissionError.name === 'OrganisationContextRequiredError'
|
|
565
|
+
);
|
|
566
|
+
}, [permissionError]);
|
|
548
567
|
|
|
549
568
|
// Handle permission check results with audit logging and callbacks
|
|
550
569
|
useEffect(() => {
|
|
@@ -614,9 +633,9 @@ export function PaceAppLayout({
|
|
|
614
633
|
const hasOrganisationContext = currentScope.organisationId;
|
|
615
634
|
const hasUser = !!user?.id;
|
|
616
635
|
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
// This prevents navigation from disappearing after initial render while waiting for
|
|
636
|
+
// Show navigation optimistically while waiting for context selection.
|
|
637
|
+
// Only hide navigation if user is not loaded.
|
|
638
|
+
// This prevents navigation from disappearing after initial render while waiting for context.
|
|
620
639
|
if (!hasUser) {
|
|
621
640
|
// User not loaded yet - show all items until user is ready
|
|
622
641
|
if (isMounted) {
|
|
@@ -625,9 +644,9 @@ export function PaceAppLayout({
|
|
|
625
644
|
return;
|
|
626
645
|
}
|
|
627
646
|
|
|
628
|
-
// If no organisation context yet, show items optimistically
|
|
629
|
-
// This allows users to see navigation while they select
|
|
630
|
-
// Only proceed with permission filtering once
|
|
647
|
+
// If no organisation context yet, show items optimistically
|
|
648
|
+
// This allows users to see navigation while they select a context
|
|
649
|
+
// Only proceed with permission filtering once context is selected
|
|
631
650
|
if (!hasOrganisationContext) {
|
|
632
651
|
if (isMounted) {
|
|
633
652
|
// Show items optimistically when org selector is enabled, otherwise show all items
|
|
@@ -682,32 +701,130 @@ export function PaceAppLayout({
|
|
|
682
701
|
scope: permissionScope,
|
|
683
702
|
});
|
|
684
703
|
|
|
685
|
-
// Filter items using the permission map
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
704
|
+
// Filter items using the permission map and scope type
|
|
705
|
+
// First, get scope types for all pages (batch if possible, or individual)
|
|
706
|
+
const { getPageScopeType } = await import('../../rbac/api');
|
|
707
|
+
const effectiveAppId = currentScope.appId || resolvedAppId;
|
|
708
|
+
const effectiveAppName = appName;
|
|
709
|
+
|
|
710
|
+
// Determine current context type for scope filtering
|
|
711
|
+
const hasEventContext = !!currentScope.eventId;
|
|
712
|
+
const hasOrgContext = !!currentScope.organisationId;
|
|
713
|
+
|
|
714
|
+
// Filter items using both permission map and scope type
|
|
715
|
+
const filtered = await Promise.all(
|
|
716
|
+
baseMenuItems.map(async (item) => {
|
|
717
|
+
if (!item.href) return { item, hasAccess: true, matchesScope: true, scopeCheckError: false };
|
|
718
|
+
|
|
719
|
+
// Extract page ID from href: remove leading slash, fallback to 'dashboard' for root
|
|
720
|
+
// This matches database page names in rbac_app_pages
|
|
721
|
+
const pageId = pageIdMapping[item.href] || (item.href === '/' ? 'dashboard' : item.href.slice(1)) || 'dashboard';
|
|
722
|
+
const permission = routePermissions[item.href] || defaultPermission;
|
|
723
|
+
const fullPermission: Permission = permission.includes(':')
|
|
724
|
+
? (permission as Permission)
|
|
725
|
+
: (pageId ? `${permission}:page.${pageId}` : permission) as Permission;
|
|
726
|
+
|
|
727
|
+
// Check permission map (super admin check already handled in getPermissionMap)
|
|
728
|
+
const hasAccess = permissionMap['*'] === true || permissionMap[fullPermission] === true;
|
|
729
|
+
|
|
730
|
+
// Check scope type compatibility
|
|
731
|
+
let matchesScope = true;
|
|
732
|
+
let scopeCheckError = false;
|
|
733
|
+
if (effectiveAppId || effectiveAppName) {
|
|
734
|
+
try {
|
|
735
|
+
const pageScopeType = await getPageScopeType(
|
|
736
|
+
pageId,
|
|
737
|
+
effectiveAppId,
|
|
738
|
+
effectiveAppName
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Filter based on current context:
|
|
742
|
+
// - If event is selected: show only 'event' or 'both' pages
|
|
743
|
+
// - If only org is selected: show only 'organisation' or 'both' pages
|
|
744
|
+
if (hasEventContext) {
|
|
745
|
+
// Event context: show 'event' or 'both' pages only
|
|
746
|
+
matchesScope = pageScopeType === 'event' || pageScopeType === 'both';
|
|
747
|
+
} else if (hasOrgContext) {
|
|
748
|
+
// Organisation context only: show 'organisation' or 'both' pages only
|
|
749
|
+
matchesScope = pageScopeType === 'organisation' || pageScopeType === 'both';
|
|
750
|
+
} else {
|
|
751
|
+
// No context: show all pages (shouldn't happen, but safe fallback)
|
|
752
|
+
matchesScope = true;
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
// If we can't get scope type, allow the page (graceful degradation)
|
|
756
|
+
// This prevents navigation from disappearing if there's a database issue
|
|
757
|
+
scopeCheckError = true;
|
|
758
|
+
logger.warn('PaceAppLayout', 'Failed to get page scope type for navigation filtering', {
|
|
759
|
+
pageId,
|
|
760
|
+
href: item.href,
|
|
761
|
+
contextType: hasEventContext ? 'event' : (hasOrgContext ? 'organisation' : 'none'),
|
|
762
|
+
error: error instanceof Error ? error.message : String(error),
|
|
763
|
+
note: 'Allowing page to prevent navigation from disappearing - this may indicate missing scope_type in database'
|
|
764
|
+
});
|
|
765
|
+
matchesScope = true; // Default to allowing if we can't check
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
// No appId/appName available - can't check scope, allow the page
|
|
769
|
+
// This happens during initial load before app context is resolved
|
|
770
|
+
matchesScope = true;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return { item, hasAccess, matchesScope, scopeCheckError };
|
|
774
|
+
})
|
|
775
|
+
);
|
|
702
776
|
|
|
703
777
|
if (!isMounted) return;
|
|
704
778
|
|
|
705
779
|
const accessibleItems = filtered
|
|
706
|
-
.filter(({ hasAccess }) => hasAccess)
|
|
780
|
+
.filter(({ hasAccess, matchesScope }) => hasAccess && matchesScope)
|
|
707
781
|
.map(({ item }) => item);
|
|
708
782
|
|
|
783
|
+
// If all items were filtered out, check if it's due to scope filtering
|
|
784
|
+
// This can happen if:
|
|
785
|
+
// 1. All pages are scoped for the other context type (expected behavior)
|
|
786
|
+
// 2. Scope type lookup failed for all pages (should fallback to showing items with permissions)
|
|
787
|
+
if (accessibleItems.length === 0 && filtered.length > 0) {
|
|
788
|
+
const itemsWithPermissions = filtered.filter(({ hasAccess }) => hasAccess);
|
|
789
|
+
const itemsFilteredByScope = filtered.filter(({ hasAccess, matchesScope }) => hasAccess && !matchesScope);
|
|
790
|
+
const itemsWithScopeErrors = filtered.filter(({ hasAccess, scopeCheckError }) => hasAccess && scopeCheckError);
|
|
791
|
+
|
|
792
|
+
// If scope checking failed for ALL items with permissions, fall back to showing them
|
|
793
|
+
// This prevents navigation from disappearing due to database/API issues
|
|
794
|
+
if (itemsWithPermissions.length > 0 &&
|
|
795
|
+
itemsWithScopeErrors.length === itemsWithPermissions.length) {
|
|
796
|
+
logger.warn('PaceAppLayout', 'Scope checking failed for all items - falling back to permission-only filtering', {
|
|
797
|
+
contextType: hasEventContext ? 'event' : (hasOrgContext ? 'organisation' : 'none'),
|
|
798
|
+
totalItems: baseMenuItems.length,
|
|
799
|
+
itemsWithPermissions: itemsWithPermissions.length,
|
|
800
|
+
scopeCheckErrorCount: itemsWithScopeErrors.length,
|
|
801
|
+
note: 'Showing items based on permissions only - scope filtering disabled due to errors. This may indicate database issues or missing scope_type values.'
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Fall back to showing items that have permissions (ignore scope check)
|
|
805
|
+
const fallbackItems = filtered
|
|
806
|
+
.filter(({ hasAccess }) => hasAccess)
|
|
807
|
+
.map(({ item }) => item);
|
|
808
|
+
|
|
809
|
+
if (isMounted && fallbackItems.length > 0) {
|
|
810
|
+
setFilteredMenuItems(fallbackItems);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
} else if (itemsWithPermissions.length > 0 && itemsFilteredByScope.length === itemsWithPermissions.length) {
|
|
814
|
+
// All items were filtered by scope (not errors) - this is expected if all pages are scoped for other context
|
|
815
|
+
logger.info('PaceAppLayout', 'All navigation items filtered out by scope type', {
|
|
816
|
+
contextType: hasEventContext ? 'event' : (hasOrgContext ? 'organisation' : 'none'),
|
|
817
|
+
totalItems: baseMenuItems.length,
|
|
818
|
+
itemsWithPermissions: itemsWithPermissions.length,
|
|
819
|
+
itemsFilteredByScope: itemsFilteredByScope.length,
|
|
820
|
+
note: 'All pages appear to be scoped for a different context type - this is expected behavior. Navigation will be empty until user selects the appropriate context.'
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
709
825
|
// SECURITY: Never show all items if permission check fails - this would be a security risk
|
|
710
826
|
// If all items are filtered out, it means the user doesn't have permission to see any navigation
|
|
827
|
+
// OR all pages are scoped for a different context type
|
|
711
828
|
// This is the correct behavior - better to show nothing than show unauthorized items
|
|
712
829
|
setFilteredMenuItems(accessibleItems);
|
|
713
830
|
} catch (error) {
|
|
@@ -725,7 +842,7 @@ export function PaceAppLayout({
|
|
|
725
842
|
return () => {
|
|
726
843
|
isMounted = false;
|
|
727
844
|
};
|
|
728
|
-
}, [baseMenuItems, pageIdMapping, routePermissions, defaultPermission, can, user?.id, scope, scopeLoading, contextAppId, resolvedScope?.appId, selectedOrganisation?.id]);
|
|
845
|
+
}, [baseMenuItems, pageIdMapping, routePermissions, defaultPermission, can, user?.id, scope, scopeLoading, contextAppId, resolvedScope?.appId, selectedOrganisation?.id, selectedEvent?.event_id, appName]);
|
|
729
846
|
|
|
730
847
|
// NEW: Phase 2 - Enhanced Routing Features
|
|
731
848
|
// Check route access for role-based routing
|
|
@@ -905,9 +1022,11 @@ export function PaceAppLayout({
|
|
|
905
1022
|
);
|
|
906
1023
|
}
|
|
907
1024
|
|
|
908
|
-
// Show permission error (only after
|
|
1025
|
+
// Show permission error (only after check is complete)
|
|
909
1026
|
// Super admins bypass all permission checks, so don't show errors for them
|
|
910
|
-
|
|
1027
|
+
// Context errors (missing event/org) should allow the page to render so it can show helpful messaging
|
|
1028
|
+
// Real permission errors should still show "Access Denied"
|
|
1029
|
+
if (enforcePermissions && permissionError && !isSuperAdmin && !isContextError) {
|
|
911
1030
|
return (
|
|
912
1031
|
<div className="flex items-center justify-center min-h-screen">
|
|
913
1032
|
<div className="text-center">
|
|
@@ -921,7 +1040,8 @@ export function PaceAppLayout({
|
|
|
921
1040
|
|
|
922
1041
|
// Show permission fallback if user lacks permission
|
|
923
1042
|
// Only show this if super admin check is complete and user is not a super admin
|
|
924
|
-
if (
|
|
1043
|
+
// Skip if it's a context error (missing event/org) - allow page to render and show helpful messaging
|
|
1044
|
+
if (enforcePermissions && hasPermission === false && !isCheckingSuperAdminDirect && !isSuperAdmin && !isContextError) {
|
|
925
1045
|
// NEW: Phase 1 - Use page permission fallback if available
|
|
926
1046
|
if (enforcePagePermissions && pagePermissionFallback) {
|
|
927
1047
|
return <>{pagePermissionFallback}</>;
|
|
@@ -974,8 +1094,9 @@ export function PaceAppLayout({
|
|
|
974
1094
|
navigate(item.href);
|
|
975
1095
|
}
|
|
976
1096
|
}}
|
|
977
|
-
|
|
978
|
-
|
|
1097
|
+
showContextSelector={showContextSelector}
|
|
1098
|
+
showOrganisations={showOrganisations}
|
|
1099
|
+
showEvents={showEvents}
|
|
979
1100
|
showUserMenu={showUserMenu}
|
|
980
1101
|
className={headerClassName || "sticky top-0 z-[40] w-full"}
|
|
981
1102
|
/>
|
|
@@ -27,8 +27,7 @@ A comprehensive application layout component that provides a consistent structur
|
|
|
27
27
|
|------|------|---------|-------------|
|
|
28
28
|
| `appName` | `string` | required | The name of the application to be displayed in the header |
|
|
29
29
|
| `navItems` | `NavigationItem[]` | optional | Navigation items for the header menu. If not provided, uses default navigation |
|
|
30
|
-
| `
|
|
31
|
-
| `showOrgSelector` | `boolean` | `false` | Show/hide organisation selector in the header |
|
|
30
|
+
| `showContextSelector` | `boolean` | `true` | Show/hide unified context selector (organisations and events) in the header |
|
|
32
31
|
| `headerActions` | `React.ReactNode` | optional | Custom actions to display in the header (between event selector and user menu) |
|
|
33
32
|
| `customLogo` | `React.ReactNode` | optional | Custom logo component (overrides default logo) |
|
|
34
33
|
| `logoHref` | `string` | `'/dashboard'` | URL to navigate to when logo is clicked (e.g., '/dashboard', '/home') |
|
|
@@ -162,7 +161,7 @@ function TeamApp() {
|
|
|
162
161
|
<PaceAppLayout
|
|
163
162
|
appName="TEAM"
|
|
164
163
|
navItems={teamNavItems}
|
|
165
|
-
|
|
164
|
+
showContextSelector={false} // Hide context selector
|
|
166
165
|
/>
|
|
167
166
|
}>
|
|
168
167
|
<Route index element={<ProfilePage />} />
|
|
@@ -231,26 +230,24 @@ function App() {
|
|
|
231
230
|
|
|
232
231
|
## When to Use Each Feature
|
|
233
232
|
|
|
234
|
-
### Show
|
|
233
|
+
### Show Context Selector (`showContextSelector={true}`) - Default
|
|
235
234
|
- Event management applications
|
|
236
235
|
- Apps with multi-tenant event contexts
|
|
237
|
-
-
|
|
236
|
+
- Multi-organisation applications
|
|
237
|
+
- Applications where users need to switch between events or organisations
|
|
238
238
|
- Event planning tools
|
|
239
239
|
- Event registration systems
|
|
240
|
+
- Hybrid apps (like pace-mint) that support both event and organisation contexts
|
|
241
|
+
- Organisation management tools
|
|
242
|
+
- Multi-tenant applications with organisation context
|
|
243
|
+
- Apps requiring organisation-scoped data access
|
|
240
244
|
|
|
241
|
-
### Hide
|
|
245
|
+
### Hide Context Selector (`showContextSelector={false}`)
|
|
242
246
|
- Personal profile management apps (like TEAM)
|
|
243
247
|
- User account settings applications
|
|
244
|
-
-
|
|
248
|
+
- Apps that don't require event or organisation context
|
|
245
249
|
- Single-tenant applications
|
|
246
|
-
- Administrative dashboards that don't depend on events
|
|
247
|
-
|
|
248
|
-
### Show Organisation Selector (`showOrgSelector={true}`)
|
|
249
|
-
- Multi-organisation applications
|
|
250
|
-
- Apps where users need to switch between organisations
|
|
251
|
-
- Organisation management tools
|
|
252
|
-
- Multi-tenant applications with organisation context
|
|
253
|
-
- Apps requiring organisation-scoped data access
|
|
250
|
+
- Administrative dashboards that don't depend on events or organisations
|
|
254
251
|
|
|
255
252
|
### Custom Header Components
|
|
256
253
|
- Applications requiring custom branding
|
|
@@ -267,7 +264,7 @@ function App() {
|
|
|
267
264
|
|
|
268
265
|
## Integration with UnifiedAuthProvider
|
|
269
266
|
|
|
270
|
-
The PaceAppLayout works seamlessly with the UnifiedAuthProvider. When `
|
|
267
|
+
The PaceAppLayout works seamlessly with the UnifiedAuthProvider. When `showContextSelector={false}`, the provider will:
|
|
271
268
|
|
|
272
269
|
- Still handle user authentication
|
|
273
270
|
- Provide user profile access
|
|
@@ -285,7 +282,7 @@ function App() {
|
|
|
285
282
|
<Route path="/" element={
|
|
286
283
|
<PaceAppLayout
|
|
287
284
|
appName="TEAM"
|
|
288
|
-
|
|
285
|
+
showContextSelector={false} // No context selection needed
|
|
289
286
|
/>
|
|
290
287
|
}>
|
|
291
288
|
<Route index element={<ProfilePage />} />
|
|
@@ -117,7 +117,7 @@ export const createMockHeader = () => ({
|
|
|
117
117
|
onChangePassword,
|
|
118
118
|
currentPath,
|
|
119
119
|
onNavigate,
|
|
120
|
-
|
|
120
|
+
showContextSelector,
|
|
121
121
|
showUserMenu,
|
|
122
122
|
className
|
|
123
123
|
}: any) => (
|
|
@@ -156,7 +156,7 @@ export const createMockHeader = () => ({
|
|
|
156
156
|
Navigate
|
|
157
157
|
</button>
|
|
158
158
|
<div data-testid="current-path">{currentPath}</div>
|
|
159
|
-
<div data-testid="show-
|
|
159
|
+
<div data-testid="show-context-selector">{showContextSelector !== false ? 'true' : 'false'}</div>
|
|
160
160
|
</header>
|
|
161
161
|
)
|
|
162
162
|
});
|
package/src/components/index.ts
CHANGED
|
@@ -215,8 +215,9 @@ export {
|
|
|
215
215
|
} from './NavigationMenu';
|
|
216
216
|
export type { NavigationMenuProps, NavigationItem } from './NavigationMenu';
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
export
|
|
218
|
+
// Unified context selector (replaces separate org/event selectors)
|
|
219
|
+
export { ContextSelector } from './ContextSelector';
|
|
220
|
+
export type { ContextSelectorProps } from './ContextSelector';
|
|
220
221
|
|
|
221
222
|
export { UserMenu } from './UserMenu';
|
|
222
223
|
|
|
@@ -235,10 +236,9 @@ export { SessionRestorationLoader } from './SessionRestorationLoader';
|
|
|
235
236
|
export type { SessionRestorationLoaderProps } from './SessionRestorationLoader';
|
|
236
237
|
|
|
237
238
|
// ============================================================================
|
|
238
|
-
//
|
|
239
|
+
// CONTEXT SELECTION
|
|
239
240
|
// ============================================================================
|
|
240
|
-
|
|
241
|
-
export { EventSelector } from './EventSelector';
|
|
241
|
+
// Note: EventSelector and OrganisationSelector removed - use ContextSelector instead
|
|
242
242
|
|
|
243
243
|
// ============================================================================
|
|
244
244
|
// AUTHENTICATION FORMS
|
|
@@ -378,6 +378,112 @@ module.exports = {
|
|
|
378
378
|
}
|
|
379
379
|
};
|
|
380
380
|
}
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Disallow direct Supabase client creation - must use useSecureSupabase
|
|
385
|
+
*/
|
|
386
|
+
'no-direct-supabase-client': {
|
|
387
|
+
meta: {
|
|
388
|
+
type: 'problem',
|
|
389
|
+
docs: {
|
|
390
|
+
description: 'Disallow direct createClient calls from @supabase/supabase-js. Use useSecureSupabase() from pace-core instead to ensure organisation context and RLS policies are enforced.',
|
|
391
|
+
category: 'Security',
|
|
392
|
+
recommended: true
|
|
393
|
+
},
|
|
394
|
+
messages: {
|
|
395
|
+
directClientCreation: "Direct Supabase client creation detected. You MUST use useSecureSupabase() from '@jmruthers/pace-core/rbac' instead to ensure organisation context and RLS policies are enforced. This prevents cross-organisation data access.",
|
|
396
|
+
directClientImport: "Direct import of createClient from @supabase/supabase-js is not allowed. Use useSecureSupabase() from '@jmruthers/pace-core/rbac' instead."
|
|
397
|
+
},
|
|
398
|
+
hasSuggestions: true
|
|
399
|
+
},
|
|
400
|
+
create(context) {
|
|
401
|
+
let hasSupabaseImport = false;
|
|
402
|
+
let createClientImported = false;
|
|
403
|
+
const filename = context.getFilename();
|
|
404
|
+
|
|
405
|
+
// Allow createClient in specific config files (supabaseClient.ts/js, etc.)
|
|
406
|
+
const isConfigFile = /(supabase|client)\.(ts|js|tsx|jsx)$/i.test(filename) &&
|
|
407
|
+
(filename.includes('supabase') || filename.includes('client'));
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
ImportDeclaration(node) {
|
|
411
|
+
const importSource = node.source.value;
|
|
412
|
+
|
|
413
|
+
// Check for @supabase/supabase-js import
|
|
414
|
+
if (importSource === '@supabase/supabase-js') {
|
|
415
|
+
hasSupabaseImport = true;
|
|
416
|
+
|
|
417
|
+
// Check if createClient is imported
|
|
418
|
+
const hasCreateClient = node.specifiers.some(spec => {
|
|
419
|
+
if (spec.type === 'ImportSpecifier') {
|
|
420
|
+
return spec.imported.name === 'createClient';
|
|
421
|
+
}
|
|
422
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
423
|
+
return true; // import * as supabase
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (hasCreateClient) {
|
|
429
|
+
createClientImported = true;
|
|
430
|
+
|
|
431
|
+
// Only report if not in a config file
|
|
432
|
+
if (!isConfigFile) {
|
|
433
|
+
context.report({
|
|
434
|
+
node: node.source,
|
|
435
|
+
messageId: 'directClientImport',
|
|
436
|
+
suggest: [{
|
|
437
|
+
desc: 'Replace with useSecureSupabase hook',
|
|
438
|
+
fix(fixer) {
|
|
439
|
+
// Remove the import
|
|
440
|
+
return fixer.remove(node);
|
|
441
|
+
}
|
|
442
|
+
}]
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
CallExpression(node) {
|
|
450
|
+
// Check for createClient() calls
|
|
451
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'createClient') {
|
|
452
|
+
// Only report if not in a config file
|
|
453
|
+
if (!isConfigFile) {
|
|
454
|
+
context.report({
|
|
455
|
+
node,
|
|
456
|
+
messageId: 'directClientCreation',
|
|
457
|
+
suggest: [{
|
|
458
|
+
desc: 'Use useSecureSupabase() hook instead',
|
|
459
|
+
fix(fixer) {
|
|
460
|
+
// This is complex to auto-fix, so we'll just report
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}]
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Check for supabase.createClient() or similar patterns
|
|
469
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
470
|
+
node.callee.property?.name === 'createClient') {
|
|
471
|
+
if (!isConfigFile) {
|
|
472
|
+
context.report({
|
|
473
|
+
node,
|
|
474
|
+
messageId: 'directClientCreation',
|
|
475
|
+
suggest: [{
|
|
476
|
+
desc: 'Use useSecureSupabase() hook instead',
|
|
477
|
+
fix(fixer) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}]
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
381
487
|
}
|
|
382
488
|
}
|
|
383
489
|
};
|