@jmruthers/pace-core 0.5.188 → 0.5.190
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/core-usage-manifest.json +0 -4
- package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
- package/dist/{DataTable-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
- package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-C4uxosp6.d.ts} +129 -40
- package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
- package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
- package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
- package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
- package/dist/chunk-4QYC5L4K.js.map +1 -0
- package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
- package/dist/chunk-73HSNNOQ.js.map +1 -0
- package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
- package/dist/chunk-DZWK57KZ.js.map +1 -0
- package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
- package/dist/chunk-HQVPB5MZ.js.map +1 -0
- package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
- package/dist/chunk-HW3OVDUF.js.map +1 -0
- package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
- package/dist/chunk-I7PSE6JW.js.map +1 -0
- package/dist/{chunk-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
- package/dist/chunk-J2XXC7R5.js.map +1 -0
- package/dist/{chunk-UNOTYLQF.js → chunk-NIU6J6OX.js} +772 -725
- package/dist/chunk-NIU6J6OX.js.map +1 -0
- package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
- package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
- package/dist/chunk-RUYZKXOD.js.map +1 -0
- package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
- package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
- package/dist/{chunk-IPCH26AG.js → chunk-STYK4OH2.js} +11 -11
- package/dist/chunk-STYK4OH2.js.map +1 -0
- package/dist/{chunk-EFCLXK7F.js → chunk-VVBAW5A5.js} +4201 -3809
- package/dist/chunk-VVBAW5A5.js.map +1 -0
- package/dist/chunk-Y4BUBBHD.js +614 -0
- package/dist/chunk-Y4BUBBHD.js.map +1 -0
- package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
- package/dist/chunk-ZSAAAMVR.js.map +1 -0
- package/dist/components.d.ts +3 -5
- package/dist/components.js +19 -23
- package/dist/components.js.map +1 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
- package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
- package/dist/hooks.d.ts +10 -5
- package/dist/hooks.js +14 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +13 -12
- package/dist/index.js +79 -73
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +76 -12
- package/dist/rbac/index.js +12 -9
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
- package/dist/utils.js +16 -16
- package/docs/README.md +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +2 -2
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +2 -2
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.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 +4 -4
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +2 -2
- package/docs/api/classes/SecureSupabaseClient.md +21 -16
- package/docs/api/classes/StorageUtils.md +7 -4
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +128 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +20 -6
- 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/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +9 -9
- 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/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.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/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +62 -16
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +2 -2
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +26 -12
- package/docs/api/interfaces/FileUploadProps.md +30 -19
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.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/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
- package/docs/api/interfaces/NavigationContextType.md +9 -9
- 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 +7 -7
- 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 +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +8 -8
- package/docs/api/interfaces/PagePermissionContextType.md +8 -8
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +3 -11
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +10 -10
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +9 -9
- package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +4 -4
- package/docs/api/interfaces/StorageFileInfo.md +7 -7
- package/docs/api/interfaces/StorageFileMetadata.md +25 -14
- package/docs/api/interfaces/StorageListOptions.md +22 -9
- package/docs/api/interfaces/StorageListResult.md +4 -4
- package/docs/api/interfaces/StorageUploadOptions.md +21 -8
- package/docs/api/interfaces/StorageUploadResult.md +6 -6
- package/docs/api/interfaces/StorageUrlOptions.md +19 -6
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.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 +53 -53
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.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 +4 -4
- package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +155 -135
- package/docs/api-reference/components.md +72 -29
- package/docs/api-reference/providers.md +2 -2
- package/docs/api-reference/rpc-functions.md +1 -0
- package/docs/best-practices/README.md +1 -1
- package/docs/best-practices/deployment.md +8 -8
- package/docs/getting-started/examples/README.md +2 -2
- package/docs/getting-started/installation-guide.md +4 -4
- package/docs/getting-started/quick-start.md +3 -3
- package/docs/migration/MIGRATION_GUIDE.md +3 -3
- package/docs/rbac/compliance/compliance-guide.md +2 -2
- package/docs/rbac/event-based-apps.md +2 -2
- package/docs/rbac/getting-started.md +2 -2
- package/docs/rbac/quick-start.md +2 -2
- package/docs/security/README.md +4 -4
- package/docs/standards/07-rbac-and-rls-standard.md +430 -7
- package/docs/troubleshooting/README.md +2 -2
- package/docs/troubleshooting/migration.md +3 -3
- package/package.json +1 -4
- package/scripts/check-pace-core-compliance.cjs +1 -1
- package/scripts/check-pace-core-compliance.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +301 -0
- package/src/__tests__/public-recipe-view.test.ts +9 -9
- package/src/__tests__/rls-policies.test.ts +197 -61
- package/src/components/AddressField/AddressField.test.tsx +42 -0
- package/src/components/AddressField/AddressField.tsx +71 -60
- package/src/components/AddressField/README.md +1 -0
- package/src/components/Alert/Alert.test.tsx +50 -10
- package/src/components/Alert/Alert.tsx +5 -3
- package/src/components/Avatar/Avatar.test.tsx +252 -226
- package/src/components/Avatar/Avatar.tsx +179 -53
- package/src/components/Avatar/index.ts +1 -1
- package/src/components/Button/Button.test.tsx +2 -1
- package/src/components/Button/Button.tsx +3 -3
- package/src/components/Calendar/Calendar.test.tsx +53 -37
- package/src/components/Calendar/Calendar.tsx +409 -82
- package/src/components/Card/Card.test.tsx +7 -4
- package/src/components/Card/Card.tsx +3 -6
- package/src/components/Checkbox/Checkbox.tsx +2 -2
- package/src/components/DataTable/components/ActionButtons.tsx +5 -5
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
- package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
- package/src/components/DataTable/components/DataTableBody.tsx +12 -12
- package/src/components/DataTable/components/DataTableCore.tsx +3 -3
- package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
- package/src/components/DataTable/components/EditableRow.tsx +2 -2
- package/src/components/DataTable/components/EmptyState.tsx +3 -3
- package/src/components/DataTable/components/GroupHeader.tsx +2 -2
- package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
- package/src/components/DataTable/components/ImportModal.tsx +4 -4
- package/src/components/DataTable/components/LoadingState.tsx +1 -1
- package/src/components/DataTable/components/PaginationControls.tsx +11 -11
- package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
- package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
- package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
- package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
- package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
- package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
- package/src/components/Dialog/Dialog.tsx +2 -2
- package/src/components/EventSelector/EventSelector.tsx +7 -7
- package/src/components/FileDisplay/FileDisplay.tsx +291 -179
- package/src/components/FileUpload/FileUpload.tsx +7 -4
- package/src/components/Header/Header.test.tsx +28 -0
- package/src/components/Header/Header.tsx +22 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
- package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +8 -8
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
- package/src/components/PaceAppLayout/test-setup.tsx +1 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
- package/src/components/Progress/Progress.test.tsx +18 -19
- package/src/components/Progress/Progress.tsx +31 -32
- package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
- package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
- package/src/components/Select/Select.tsx +5 -5
- package/src/components/Switch/Switch.test.tsx +2 -1
- package/src/components/Switch/Switch.tsx +1 -1
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/components/Tooltip/Tooltip.test.tsx +8 -2
- package/src/components/UserMenu/UserMenu.test.tsx +7 -9
- package/src/components/UserMenu/UserMenu.tsx +10 -8
- package/src/components/index.ts +2 -1
- package/src/eslint-rules/pace-core-compliance.cjs +0 -2
- package/src/eslint-rules/pace-core-compliance.js +0 -2
- package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
- package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
- package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
- package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
- package/src/hooks/__tests__/{usePublicEvent.unit.test.ts → usePublicEvent.test.ts} +28 -1
- package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEvent.ts +2 -2
- package/src/hooks/public/usePublicFileDisplay.ts +173 -87
- package/src/hooks/useAppConfig.ts +24 -5
- package/src/hooks/useFileDisplay.ts +297 -34
- package/src/hooks/useFileReference.ts +56 -11
- package/src/hooks/useFileUrl.ts +1 -1
- package/src/hooks/useInactivityTracker.ts +16 -7
- package/src/hooks/usePermissionCache.test.ts +85 -8
- package/src/hooks/useQueryCache.ts +21 -0
- package/src/hooks/useSecureDataAccess.test.ts +80 -35
- package/src/hooks/useSecureDataAccess.ts +80 -37
- package/src/index.ts +2 -1
- package/src/providers/services/EventServiceProvider.tsx +37 -17
- package/src/providers/services/InactivityServiceProvider.tsx +4 -4
- package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
- package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
- package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
- package/src/rbac/api.ts +240 -36
- package/src/rbac/cache-invalidation.ts +21 -7
- package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
- package/src/rbac/components/NavigationGuard.tsx +23 -63
- package/src/rbac/components/NavigationProvider.test.tsx +52 -23
- package/src/rbac/components/NavigationProvider.tsx +13 -11
- package/src/rbac/components/PagePermissionGuard.tsx +77 -203
- package/src/rbac/components/PagePermissionProvider.tsx +13 -11
- package/src/rbac/components/PermissionEnforcer.tsx +24 -62
- package/src/rbac/components/RoleBasedRouter.tsx +14 -12
- package/src/rbac/components/SecureDataProvider.tsx +13 -11
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
- package/src/rbac/engine.ts +4 -2
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
- package/src/rbac/hooks/index.ts +3 -0
- package/src/rbac/hooks/useCan.test.ts +101 -53
- package/src/rbac/hooks/usePermissions.ts +108 -41
- package/src/rbac/hooks/useRBAC.test.ts +11 -3
- package/src/rbac/hooks/useRBAC.ts +83 -40
- package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
- package/src/rbac/hooks/useResolvedScope.ts +128 -70
- package/src/rbac/hooks/useSecureSupabase.ts +36 -19
- package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
- package/src/rbac/request-deduplication.ts +1 -1
- package/src/rbac/secureClient.ts +72 -12
- package/src/rbac/security.ts +29 -23
- package/src/rbac/types.ts +10 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
- package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +6 -1
- package/src/rbac/utils/contextValidator.ts +288 -0
- package/src/rbac/utils/eventContext.ts +48 -2
- package/src/services/EventService.ts +165 -21
- package/src/services/OrganisationService.ts +37 -2
- package/src/services/__tests__/EventService.test.ts +26 -21
- package/src/types/file-reference.ts +13 -10
- package/src/utils/app/appNameResolver.test.ts +346 -73
- package/src/utils/context/superAdminOverride.ts +58 -0
- package/src/utils/file-reference/index.ts +61 -33
- package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
- package/src/utils/storage/helpers.test.ts +1 -1
- package/src/utils/storage/helpers.ts +38 -19
- package/src/utils/storage/types.ts +15 -8
- package/src/utils/validation/__tests__/csrf.test.ts +105 -0
- package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
- package/src/vite-env.d.ts +2 -2
- package/dist/chunk-3GOZZZYH.js.map +0 -1
- package/dist/chunk-DDM4CCYT.js.map +0 -1
- package/dist/chunk-E7UAOUMY.js +0 -75
- package/dist/chunk-E7UAOUMY.js.map +0 -1
- package/dist/chunk-EFCLXK7F.js.map +0 -1
- package/dist/chunk-F2IMUDXZ.js.map +0 -1
- package/dist/chunk-HEHYGYOX.js.map +0 -1
- package/dist/chunk-IM4QE42D.js.map +0 -1
- package/dist/chunk-IPCH26AG.js.map +0 -1
- package/dist/chunk-SAUPYVLF.js.map +0 -1
- package/dist/chunk-THRPYOFK.js.map +0 -1
- package/dist/chunk-UNOTYLQF.js.map +0 -1
- package/dist/chunk-VGZZXKBR.js.map +0 -1
- package/dist/chunk-YHCN776L.js.map +0 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
- package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
- /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
- /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
- /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Comprehensive tests for the usePermissionCache hook covering all critical functionality.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { renderHook,
|
|
11
|
-
import { vi, describe, it, expect, beforeEach
|
|
10
|
+
import { renderHook, act } from '@testing-library/react';
|
|
11
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
12
12
|
import { usePermissionCache } from './usePermissionCache';
|
|
13
13
|
|
|
14
14
|
// Mock the dependencies
|
|
@@ -29,6 +29,16 @@ vi.mock('../rbac/api', () => ({
|
|
|
29
29
|
isPermittedCached: vi.fn()
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
+
const loggerSpy = vi.hoisted(() => ({
|
|
33
|
+
warn: vi.fn(),
|
|
34
|
+
error: vi.fn()
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('../utils/core/logger', () => ({
|
|
38
|
+
logger: loggerSpy,
|
|
39
|
+
createLogger: () => loggerSpy
|
|
40
|
+
}));
|
|
41
|
+
|
|
32
42
|
import { useUnifiedAuth } from '../providers/services/UnifiedAuthProvider';
|
|
33
43
|
import { useOrganisations } from './useOrganisations';
|
|
34
44
|
import { useEvents } from './useEvents';
|
|
@@ -41,7 +51,7 @@ const mockIsPermittedCached = vi.mocked(isPermittedCached);
|
|
|
41
51
|
describe('usePermissionCache', () => {
|
|
42
52
|
beforeEach(() => {
|
|
43
53
|
vi.clearAllMocks();
|
|
44
|
-
|
|
54
|
+
|
|
45
55
|
// Setup default mocks
|
|
46
56
|
mockUseUnifiedAuthFn.mockReturnValue({
|
|
47
57
|
user: { id: 'user-123' },
|
|
@@ -50,19 +60,19 @@ describe('usePermissionCache', () => {
|
|
|
50
60
|
selectedOrganisationId: 'org-123',
|
|
51
61
|
selectedEventId: undefined
|
|
52
62
|
} as any);
|
|
53
|
-
|
|
63
|
+
|
|
54
64
|
mockUseOrganisations.mockReturnValue({
|
|
55
65
|
selectedOrganisation: { id: 'org-123' }
|
|
56
66
|
} as any);
|
|
57
|
-
|
|
67
|
+
|
|
58
68
|
mockUseEvents.mockReturnValue({
|
|
59
69
|
selectedEvent: null
|
|
60
70
|
} as any);
|
|
61
|
-
|
|
71
|
+
|
|
62
72
|
mockIsPermittedCached.mockResolvedValue(true);
|
|
63
73
|
});
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
describe('Hook Initialization', () => {
|
|
66
76
|
it('initializes with default configuration', () => {
|
|
67
77
|
const { result } = renderHook(() => usePermissionCache());
|
|
68
78
|
|
|
@@ -130,6 +140,16 @@ describe('usePermissionCache', () => {
|
|
|
130
140
|
expect(results.every(r => r.hasPermission)).toBe(true);
|
|
131
141
|
});
|
|
132
142
|
|
|
143
|
+
it('handles empty permission arrays gracefully', async () => {
|
|
144
|
+
const { result } = renderHook(() => usePermissionCache());
|
|
145
|
+
|
|
146
|
+
const results = await result.current.checkMultiplePermissions([]);
|
|
147
|
+
|
|
148
|
+
expect(results).toEqual([]);
|
|
149
|
+
expect(mockIsPermittedCached).not.toHaveBeenCalled();
|
|
150
|
+
expect(loggerSpy.warn).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
133
153
|
it('handles permission check errors gracefully', async () => {
|
|
134
154
|
mockIsPermittedCached.mockRejectedValueOnce(new Error('Permission check failed'));
|
|
135
155
|
|
|
@@ -137,6 +157,7 @@ describe('usePermissionCache', () => {
|
|
|
137
157
|
|
|
138
158
|
const hasPermission = await result.current.checkPermission('read', 'users');
|
|
139
159
|
expect(hasPermission).toBe(false);
|
|
160
|
+
expect(loggerSpy.error).toHaveBeenCalled();
|
|
140
161
|
});
|
|
141
162
|
|
|
142
163
|
it('handles missing user context gracefully', () => {
|
|
@@ -153,6 +174,23 @@ describe('usePermissionCache', () => {
|
|
|
153
174
|
expect(result.current).toBeDefined();
|
|
154
175
|
expect(result.current.checkPermission).toBeInstanceOf(Function);
|
|
155
176
|
});
|
|
177
|
+
|
|
178
|
+
it('deduplicates concurrent permission checks', async () => {
|
|
179
|
+
mockIsPermittedCached.mockImplementation(
|
|
180
|
+
() => new Promise(resolve => setTimeout(() => resolve(true), 50))
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const { result } = renderHook(() => usePermissionCache());
|
|
184
|
+
|
|
185
|
+
const [first, second] = await Promise.all([
|
|
186
|
+
result.current.checkPermission('read', 'users'),
|
|
187
|
+
result.current.checkPermission('read', 'users')
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
expect(first).toBe(true);
|
|
191
|
+
expect(second).toBe(true);
|
|
192
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(1);
|
|
193
|
+
});
|
|
156
194
|
});
|
|
157
195
|
|
|
158
196
|
describe('Cache Management', () => {
|
|
@@ -196,10 +234,23 @@ describe('usePermissionCache', () => {
|
|
|
196
234
|
|
|
197
235
|
// Check again - should not use cache
|
|
198
236
|
await result.current.checkPermission('read', 'users');
|
|
199
|
-
|
|
237
|
+
|
|
200
238
|
expect(mockIsPermittedCached).toHaveBeenCalledTimes(2);
|
|
201
239
|
});
|
|
202
240
|
|
|
241
|
+
it('invalidates cache entries by pattern', async () => {
|
|
242
|
+
const { result } = renderHook(() => usePermissionCache());
|
|
243
|
+
|
|
244
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
245
|
+
await result.current.checkPermission('read', 'admin');
|
|
246
|
+
|
|
247
|
+
result.current.invalidateCache('dashboard');
|
|
248
|
+
|
|
249
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
250
|
+
|
|
251
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(3);
|
|
252
|
+
});
|
|
253
|
+
|
|
203
254
|
it('respects max cache size', async () => {
|
|
204
255
|
const { result } = renderHook(() => usePermissionCache({
|
|
205
256
|
maxCacheSize: 2
|
|
@@ -246,6 +297,32 @@ describe('usePermissionCache', () => {
|
|
|
246
297
|
expect(debugInfo.totalChecks).toBe(2);
|
|
247
298
|
});
|
|
248
299
|
|
|
300
|
+
it('returns cached permissions for a page', async () => {
|
|
301
|
+
mockIsPermittedCached
|
|
302
|
+
.mockResolvedValueOnce(true)
|
|
303
|
+
.mockResolvedValueOnce(false)
|
|
304
|
+
.mockResolvedValueOnce(true)
|
|
305
|
+
.mockResolvedValueOnce(false);
|
|
306
|
+
|
|
307
|
+
const { result } = renderHook(() => usePermissionCache());
|
|
308
|
+
|
|
309
|
+
await result.current.checkMultiplePermissions([
|
|
310
|
+
['read', 'dashboard'],
|
|
311
|
+
['create', 'dashboard'],
|
|
312
|
+
['update', 'dashboard'],
|
|
313
|
+
['delete', 'dashboard']
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
const cachedPermissions = result.current.getCachedPermissions('dashboard');
|
|
317
|
+
|
|
318
|
+
expect(cachedPermissions).toEqual([
|
|
319
|
+
{ operation: 'read', hasPermission: true },
|
|
320
|
+
{ operation: 'create', hasPermission: false },
|
|
321
|
+
{ operation: 'update', hasPermission: true },
|
|
322
|
+
{ operation: 'delete', hasPermission: false }
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
|
|
249
326
|
it('tracks average response time', async () => {
|
|
250
327
|
const { result } = renderHook(() => usePermissionCache());
|
|
251
328
|
|
|
@@ -51,6 +51,27 @@ function runCacheCleanup() {
|
|
|
51
51
|
if (typeof window !== 'undefined' && !cleanupTimer) {
|
|
52
52
|
cleanupTimer = setInterval(runCacheCleanup, CLEANUP_INTERVAL_MS);
|
|
53
53
|
log.debug('Query cache cleanup initialized.');
|
|
54
|
+
|
|
55
|
+
// Cleanup on page unload to prevent memory leaks
|
|
56
|
+
window.addEventListener('beforeunload', () => {
|
|
57
|
+
if (cleanupTimer) {
|
|
58
|
+
clearInterval(cleanupTimer);
|
|
59
|
+
cleanupTimer = null;
|
|
60
|
+
log.debug('Query cache cleanup timer cleared on page unload.');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Manually cleanup the query cache cleanup timer
|
|
67
|
+
* Useful for testing or when the module is reloaded
|
|
68
|
+
*/
|
|
69
|
+
export function cleanupQueryCache(): void {
|
|
70
|
+
if (cleanupTimer) {
|
|
71
|
+
clearInterval(cleanupTimer);
|
|
72
|
+
cleanupTimer = null;
|
|
73
|
+
log.debug('Query cache cleanup timer cleared.');
|
|
74
|
+
}
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
export interface UseQueryCacheOptions {
|
|
@@ -14,6 +14,8 @@ import { useUnifiedAuth } from '../providers';
|
|
|
14
14
|
import { useOrganisations } from './useOrganisations';
|
|
15
15
|
import { setOrganisationContext } from '../utils/context/organisationContext';
|
|
16
16
|
import { createMockSupabaseClient, createMockQueryBuilderWithData } from '../__tests__/helpers/supabaseMock';
|
|
17
|
+
import { useResolvedScope } from '../rbac/hooks/useResolvedScope';
|
|
18
|
+
import { useSuperAdminBypass } from '../rbac/hooks/useSuperAdminBypass';
|
|
17
19
|
|
|
18
20
|
// Mock the providers
|
|
19
21
|
vi.mock('../providers', () => ({
|
|
@@ -29,11 +31,23 @@ vi.mock('../utils/context/organisationContext', () => ({
|
|
|
29
31
|
setOrganisationContext: vi.fn()
|
|
30
32
|
}));
|
|
31
33
|
|
|
34
|
+
// Mock useResolvedScope
|
|
35
|
+
vi.mock('../rbac/hooks/useResolvedScope', () => ({
|
|
36
|
+
useResolvedScope: vi.fn()
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Mock useSuperAdminBypass
|
|
40
|
+
vi.mock('../rbac/hooks/useSuperAdminBypass', () => ({
|
|
41
|
+
useSuperAdminBypass: vi.fn()
|
|
42
|
+
}));
|
|
43
|
+
|
|
32
44
|
|
|
33
45
|
describe('useSecureDataAccess', () => {
|
|
34
46
|
const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
|
|
35
47
|
const mockUseOrganisations = vi.mocked(useOrganisations);
|
|
36
48
|
const mockSetOrganisationContext = vi.mocked(setOrganisationContext);
|
|
49
|
+
const mockUseResolvedScope = vi.mocked(useResolvedScope);
|
|
50
|
+
const mockUseSuperAdminBypass = vi.mocked(useSuperAdminBypass);
|
|
37
51
|
|
|
38
52
|
const mockUser = {
|
|
39
53
|
id: 'user-123',
|
|
@@ -93,6 +107,22 @@ describe('useSecureDataAccess', () => {
|
|
|
93
107
|
ensureOrganisationContext: vi.fn().mockReturnValue(mockSelectedOrganisation),
|
|
94
108
|
// Add other required properties
|
|
95
109
|
} as any);
|
|
110
|
+
|
|
111
|
+
// Mock useResolvedScope to return resolved scope with organisationId
|
|
112
|
+
mockUseResolvedScope.mockReturnValue({
|
|
113
|
+
resolvedScope: {
|
|
114
|
+
organisationId: 'org-123',
|
|
115
|
+
eventId: undefined,
|
|
116
|
+
appId: undefined,
|
|
117
|
+
},
|
|
118
|
+
isLoading: false,
|
|
119
|
+
error: null,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Mock useSuperAdminBypass to return not super admin
|
|
123
|
+
mockUseSuperAdminBypass.mockReturnValue({
|
|
124
|
+
isSuperAdmin: false,
|
|
125
|
+
});
|
|
96
126
|
});
|
|
97
127
|
|
|
98
128
|
describe('Hook Initialization', () => {
|
|
@@ -113,9 +143,10 @@ describe('useSecureDataAccess', () => {
|
|
|
113
143
|
expect(mockUseUnifiedAuth).toHaveBeenCalled();
|
|
114
144
|
});
|
|
115
145
|
|
|
116
|
-
it('depends on
|
|
146
|
+
it('depends on useResolvedScope hook', () => {
|
|
117
147
|
renderHook(() => useSecureDataAccess());
|
|
118
|
-
|
|
148
|
+
// useSecureDataAccess now uses useResolvedScope instead of useOrganisations
|
|
149
|
+
expect(mockUseResolvedScope).toHaveBeenCalled();
|
|
119
150
|
});
|
|
120
151
|
});
|
|
121
152
|
|
|
@@ -328,15 +359,22 @@ describe('useSecureDataAccess', () => {
|
|
|
328
359
|
});
|
|
329
360
|
|
|
330
361
|
it('handles missing organisation context', () => {
|
|
331
|
-
|
|
362
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
363
|
+
user: mockUser,
|
|
364
|
+
session: mockSession,
|
|
365
|
+
supabase: freshMockSupabase,
|
|
366
|
+
isAuthenticated: true,
|
|
367
|
+
signOut: vi.fn(),
|
|
332
368
|
selectedOrganisation: null,
|
|
333
|
-
|
|
334
|
-
validateOrganisationAccess: vi.fn().mockResolvedValue(false),
|
|
335
|
-
ensureOrganisationContext: vi.fn().mockImplementation(() => {
|
|
336
|
-
throw new Error('Organisation context is required but not available');
|
|
337
|
-
}),
|
|
338
|
-
// Add other required properties
|
|
369
|
+
selectedEvent: null,
|
|
339
370
|
} as any);
|
|
371
|
+
|
|
372
|
+
// Mock useResolvedScope to return null scope when no context available
|
|
373
|
+
mockUseResolvedScope.mockReturnValue({
|
|
374
|
+
resolvedScope: null,
|
|
375
|
+
isLoading: false,
|
|
376
|
+
error: null,
|
|
377
|
+
});
|
|
340
378
|
|
|
341
379
|
const { result } = renderHook(() => useSecureDataAccess());
|
|
342
380
|
|
|
@@ -344,21 +382,15 @@ describe('useSecureDataAccess', () => {
|
|
|
344
382
|
});
|
|
345
383
|
|
|
346
384
|
it('validates organisation access before operations', async () => {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
selectedOrganisation: mockSelectedOrganisation,
|
|
350
|
-
getUserRole: vi.fn().mockReturnValue('member'),
|
|
351
|
-
validateOrganisationAccess: mockValidateAccess,
|
|
352
|
-
ensureOrganisationContext: vi.fn().mockReturnValue(mockSelectedOrganisation),
|
|
353
|
-
// Add other required properties
|
|
354
|
-
} as any);
|
|
355
|
-
|
|
385
|
+
// The hook uses useResolvedScope to get organisationId, which is already mocked in beforeEach
|
|
386
|
+
// The validation happens in validateContext which checks resolvedScope.organisationId
|
|
356
387
|
const { result } = renderHook(() => useSecureDataAccess());
|
|
357
388
|
|
|
358
|
-
|
|
389
|
+
// Use 'event' table which is in the tablesWithOrganisation list
|
|
390
|
+
await result.current.secureQuery('event', '*');
|
|
359
391
|
|
|
360
|
-
//
|
|
361
|
-
expect(
|
|
392
|
+
// Verify that the query was executed with organisation filter
|
|
393
|
+
expect(freshMockQueryBuilder.eq).toHaveBeenCalledWith('organisation_id', 'org-123');
|
|
362
394
|
});
|
|
363
395
|
});
|
|
364
396
|
|
|
@@ -394,16 +426,16 @@ describe('useSecureDataAccess', () => {
|
|
|
394
426
|
});
|
|
395
427
|
|
|
396
428
|
it('handles organisation access validation failures', async () => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
429
|
+
// Mock useResolvedScope to return null scope to simulate missing context
|
|
430
|
+
mockUseResolvedScope.mockReturnValue({
|
|
431
|
+
resolvedScope: null,
|
|
432
|
+
isLoading: false,
|
|
433
|
+
error: null,
|
|
434
|
+
});
|
|
403
435
|
|
|
404
436
|
const { result } = renderHook(() => useSecureDataAccess());
|
|
405
437
|
|
|
406
|
-
await expect(result.current.secureQuery('users', '*')).rejects.toThrow();
|
|
438
|
+
await expect(result.current.secureQuery('users', '*')).rejects.toThrow('Organisation context is required for data access');
|
|
407
439
|
});
|
|
408
440
|
});
|
|
409
441
|
|
|
@@ -467,13 +499,25 @@ describe('useSecureDataAccess', () => {
|
|
|
467
499
|
|
|
468
500
|
// Change organisation
|
|
469
501
|
const newOrganisation = { id: 'org-456', name: 'New Organisation' };
|
|
470
|
-
|
|
502
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
503
|
+
user: mockUser,
|
|
504
|
+
session: mockSession,
|
|
505
|
+
supabase: freshMockSupabase,
|
|
506
|
+
isAuthenticated: true,
|
|
507
|
+
signOut: vi.fn(),
|
|
471
508
|
selectedOrganisation: newOrganisation,
|
|
472
|
-
getUserRole: vi.fn().mockReturnValue('member'),
|
|
473
|
-
validateOrganisationAccess: vi.fn().mockResolvedValue(true),
|
|
474
|
-
ensureOrganisationContext: vi.fn().mockReturnValue(newOrganisation),
|
|
475
|
-
// Add other required properties
|
|
476
509
|
} as any);
|
|
510
|
+
|
|
511
|
+
// Update useResolvedScope mock to return new organisation
|
|
512
|
+
mockUseResolvedScope.mockReturnValue({
|
|
513
|
+
resolvedScope: {
|
|
514
|
+
organisationId: 'org-456',
|
|
515
|
+
eventId: undefined,
|
|
516
|
+
appId: undefined,
|
|
517
|
+
},
|
|
518
|
+
isLoading: false,
|
|
519
|
+
error: null,
|
|
520
|
+
});
|
|
477
521
|
|
|
478
522
|
rerender();
|
|
479
523
|
|
|
@@ -487,9 +531,10 @@ describe('useSecureDataAccess', () => {
|
|
|
487
531
|
expect(mockUseUnifiedAuth).toHaveBeenCalled();
|
|
488
532
|
});
|
|
489
533
|
|
|
490
|
-
it('integrates with
|
|
534
|
+
it('integrates with useResolvedScope hook correctly', () => {
|
|
491
535
|
renderHook(() => useSecureDataAccess());
|
|
492
|
-
|
|
536
|
+
// useSecureDataAccess now uses useResolvedScope instead of useOrganisations
|
|
537
|
+
expect(mockUseResolvedScope).toHaveBeenCalled();
|
|
493
538
|
});
|
|
494
539
|
|
|
495
540
|
it('uses organisation context from provider', () => {
|
|
@@ -59,6 +59,8 @@ import { setOrganisationContext } from '../utils/context/organisationContext';
|
|
|
59
59
|
import { logger } from '../utils/core/logger';
|
|
60
60
|
import type { Permission } from '../rbac/types';
|
|
61
61
|
import type { OrganisationSecurityError } from '../types/organisation';
|
|
62
|
+
import { useSuperAdminBypass } from '../rbac/hooks/useSuperAdminBypass';
|
|
63
|
+
import { useResolvedScope } from '../rbac/hooks/useResolvedScope';
|
|
62
64
|
|
|
63
65
|
export interface SecureDataAccessReturn {
|
|
64
66
|
/** Execute a secure query with organisation filtering */
|
|
@@ -149,13 +151,21 @@ export interface DataAccessRecord {
|
|
|
149
151
|
* - RPC calls include organisation_id parameter
|
|
150
152
|
*/
|
|
151
153
|
export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
152
|
-
const { supabase, user, session } = useUnifiedAuth();
|
|
153
|
-
const { ensureOrganisationContext } = useOrganisations();
|
|
154
|
+
const { supabase, user, session, selectedOrganisation, selectedEvent } = useUnifiedAuth();
|
|
154
155
|
|
|
155
156
|
// Get selected event for event-scoped RPC calls
|
|
156
157
|
// Use useContext directly to safely check if EventServiceProvider is available
|
|
157
158
|
const eventServiceContext = useContext(EventServiceContext);
|
|
158
|
-
const
|
|
159
|
+
const eventFromContext = eventServiceContext?.eventService?.getSelectedEvent() || null;
|
|
160
|
+
const effectiveSelectedEvent = selectedEvent || eventFromContext;
|
|
161
|
+
const { isSuperAdmin } = useSuperAdminBypass();
|
|
162
|
+
|
|
163
|
+
// Use resolved scope to get organisationId (derived from event if needed)
|
|
164
|
+
const { resolvedScope } = useResolvedScope({
|
|
165
|
+
supabase,
|
|
166
|
+
selectedOrganisationId: selectedOrganisation?.id || null,
|
|
167
|
+
selectedEventId: effectiveSelectedEvent?.event_id || null
|
|
168
|
+
});
|
|
159
169
|
|
|
160
170
|
const validateContext = useCallback((): void => {
|
|
161
171
|
if (!supabase) {
|
|
@@ -165,23 +175,29 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
165
175
|
throw new Error('User must be authenticated with valid session') as OrganisationSecurityError;
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
178
|
+
if (isSuperAdmin) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!resolvedScope?.organisationId) {
|
|
171
183
|
throw new Error('Organisation context is required for data access') as OrganisationSecurityError;
|
|
172
184
|
}
|
|
173
|
-
}, [supabase, user, session,
|
|
185
|
+
}, [supabase, user, session, resolvedScope, isSuperAdmin]);
|
|
174
186
|
|
|
175
187
|
const getCurrentOrganisationId = useCallback((): string => {
|
|
188
|
+
if (isSuperAdmin) {
|
|
189
|
+
// For super admins, try to get org from resolved scope or selectedOrganisation
|
|
190
|
+
return resolvedScope?.organisationId || selectedOrganisation?.id || '';
|
|
191
|
+
}
|
|
192
|
+
|
|
176
193
|
validateContext();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}, [validateContext, ensureOrganisationContext]);
|
|
194
|
+
return resolvedScope?.organisationId || '';
|
|
195
|
+
}, [validateContext, resolvedScope, selectedOrganisation, isSuperAdmin]);
|
|
180
196
|
|
|
181
197
|
// Set organisation context in database session
|
|
182
|
-
const setOrganisationContextInSession = useCallback(async (organisationId
|
|
183
|
-
if (!supabase) {
|
|
184
|
-
|
|
198
|
+
const setOrganisationContextInSession = useCallback(async (organisationId?: string): Promise<void> => {
|
|
199
|
+
if (!supabase || !organisationId) {
|
|
200
|
+
return;
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
await setOrganisationContext(supabase, organisationId);
|
|
@@ -199,7 +215,8 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
199
215
|
} = {}
|
|
200
216
|
): Promise<T[]> => {
|
|
201
217
|
validateContext();
|
|
202
|
-
const
|
|
218
|
+
const bypassOrganisationFilter = isSuperAdmin;
|
|
219
|
+
const organisationId = bypassOrganisationFilter ? undefined : getCurrentOrganisationId();
|
|
203
220
|
|
|
204
221
|
// Set organisation context in database session
|
|
205
222
|
await setOrganisationContextInSession(organisationId);
|
|
@@ -210,6 +227,9 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
210
227
|
.select(columns);
|
|
211
228
|
|
|
212
229
|
// Add organisation filter only if table has organisation_id column
|
|
230
|
+
// Defense in depth strategy:
|
|
231
|
+
// - RLS policies are the primary security layer (cannot be bypassed)
|
|
232
|
+
// - Application-level filtering adds an additional layer of protection
|
|
213
233
|
const tablesWithOrganisation = [
|
|
214
234
|
'event', 'organisation_settings',
|
|
215
235
|
'rbac_event_app_roles', 'rbac_organisation_roles',
|
|
@@ -226,11 +246,22 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
226
246
|
'form_contexts', 'form_field_config', 'form_fields',
|
|
227
247
|
'cake_delivery', 'cake_diettype', 'cake_diner', 'cake_dish', 'cake_item',
|
|
228
248
|
'cake_logistics', 'cake_mealplan', 'cake_package', 'cake_recipe', 'cake_supplier',
|
|
229
|
-
'cake_supply', 'cake_unit', 'event_app_access', 'base_application', 'base_questions'
|
|
249
|
+
'cake_supply', 'cake_unit', 'event_app_access', 'base_application', 'base_questions',
|
|
250
|
+
// rbac_user_profiles has organisation_id but uses conditional filtering
|
|
251
|
+
'rbac_user_profiles'
|
|
230
252
|
];
|
|
231
253
|
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
// Apply organisation filtering based on table and super admin status
|
|
255
|
+
if (!bypassOrganisationFilter && organisationId && tablesWithOrganisation.includes(table)) {
|
|
256
|
+
// For rbac_user_profiles: Super admins see all (no filter), non-super-admins get filtered (defense in depth)
|
|
257
|
+
// For other tables: Always apply filter
|
|
258
|
+
if (table === 'rbac_user_profiles' && isSuperAdmin) {
|
|
259
|
+
// Super admin: No org filter - RLS handles access control
|
|
260
|
+
// This allows super admins to see all users across all organisations
|
|
261
|
+
} else {
|
|
262
|
+
// Non-super-admin OR other tables: Apply org filter as defense in depth
|
|
263
|
+
query = query.eq('organisation_id', organisationId);
|
|
264
|
+
}
|
|
234
265
|
}
|
|
235
266
|
|
|
236
267
|
// Apply additional filters
|
|
@@ -272,23 +303,26 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
272
303
|
recordDataAccess(table, 'read', true, `SELECT ${columns} FROM ${table}`, filters);
|
|
273
304
|
|
|
274
305
|
return (data as T[]) || [];
|
|
275
|
-
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase]);
|
|
306
|
+
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, isSuperAdmin]);
|
|
276
307
|
|
|
277
308
|
const secureInsert = useCallback(async <T = any>(
|
|
278
309
|
table: string,
|
|
279
310
|
data: Record<string, any>
|
|
280
311
|
): Promise<T> => {
|
|
281
312
|
validateContext();
|
|
282
|
-
const
|
|
313
|
+
const bypassOrganisationFilter = isSuperAdmin;
|
|
314
|
+
const organisationId = bypassOrganisationFilter ? undefined : getCurrentOrganisationId();
|
|
283
315
|
|
|
284
316
|
// Set organisation context in database session
|
|
285
317
|
await setOrganisationContextInSession(organisationId);
|
|
286
318
|
|
|
287
319
|
// Ensure organisation_id is set
|
|
288
|
-
const secureData =
|
|
289
|
-
...data
|
|
290
|
-
|
|
291
|
-
|
|
320
|
+
const secureData = bypassOrganisationFilter
|
|
321
|
+
? { ...data }
|
|
322
|
+
: {
|
|
323
|
+
...data,
|
|
324
|
+
organisation_id: organisationId
|
|
325
|
+
};
|
|
292
326
|
|
|
293
327
|
const { data: insertData, error } = await supabase!
|
|
294
328
|
.from(table)
|
|
@@ -302,7 +336,7 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
302
336
|
}
|
|
303
337
|
|
|
304
338
|
return insertData as T;
|
|
305
|
-
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase]);
|
|
339
|
+
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, isSuperAdmin]);
|
|
306
340
|
|
|
307
341
|
const secureUpdate = useCallback(async <T = any>(
|
|
308
342
|
table: string,
|
|
@@ -310,7 +344,8 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
310
344
|
filters: Record<string, any>
|
|
311
345
|
): Promise<T[]> => {
|
|
312
346
|
validateContext();
|
|
313
|
-
const
|
|
347
|
+
const bypassOrganisationFilter = isSuperAdmin;
|
|
348
|
+
const organisationId = bypassOrganisationFilter ? undefined : getCurrentOrganisationId();
|
|
314
349
|
|
|
315
350
|
// Set organisation context in database session
|
|
316
351
|
await setOrganisationContextInSession(organisationId);
|
|
@@ -333,7 +368,7 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
333
368
|
'cake_meal', 'cake_mealtype', 'pace_person', 'pace_member'
|
|
334
369
|
];
|
|
335
370
|
|
|
336
|
-
if (tablesWithOrganisation.includes(table)) {
|
|
371
|
+
if (!bypassOrganisationFilter && organisationId && tablesWithOrganisation.includes(table)) {
|
|
337
372
|
query = query.eq('organisation_id', organisationId);
|
|
338
373
|
}
|
|
339
374
|
|
|
@@ -352,14 +387,15 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
352
387
|
}
|
|
353
388
|
|
|
354
389
|
return (updateData as T[]) || [];
|
|
355
|
-
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase]);
|
|
390
|
+
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, isSuperAdmin]);
|
|
356
391
|
|
|
357
392
|
const secureDelete = useCallback(async (
|
|
358
393
|
table: string,
|
|
359
394
|
filters: Record<string, any>
|
|
360
395
|
): Promise<void> => {
|
|
361
396
|
validateContext();
|
|
362
|
-
const
|
|
397
|
+
const bypassOrganisationFilter = isSuperAdmin;
|
|
398
|
+
const organisationId = bypassOrganisationFilter ? undefined : getCurrentOrganisationId();
|
|
363
399
|
|
|
364
400
|
// Set organisation context in database session
|
|
365
401
|
await setOrganisationContextInSession(organisationId);
|
|
@@ -389,7 +425,7 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
389
425
|
'cake_supply', 'cake_unit', 'event_app_access', 'base_application', 'base_questions'
|
|
390
426
|
];
|
|
391
427
|
|
|
392
|
-
if (tablesWithOrganisation.includes(table)) {
|
|
428
|
+
if (!bypassOrganisationFilter && organisationId && tablesWithOrganisation.includes(table)) {
|
|
393
429
|
query = query.eq('organisation_id', organisationId);
|
|
394
430
|
}
|
|
395
431
|
|
|
@@ -406,14 +442,15 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
406
442
|
logger.error('useSecureDataAccess', 'Delete failed', { table, filters, error });
|
|
407
443
|
throw error;
|
|
408
444
|
}
|
|
409
|
-
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase]);
|
|
445
|
+
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, isSuperAdmin]);
|
|
410
446
|
|
|
411
447
|
const secureRpc = useCallback(async <T = any>(
|
|
412
448
|
functionName: string,
|
|
413
449
|
params: Record<string, any> = {}
|
|
414
450
|
): Promise<T> => {
|
|
415
451
|
validateContext();
|
|
416
|
-
const
|
|
452
|
+
const bypassOrganisationFilter = isSuperAdmin;
|
|
453
|
+
const organisationId = bypassOrganisationFilter ? undefined : getCurrentOrganisationId();
|
|
417
454
|
|
|
418
455
|
// Set organisation context in database session
|
|
419
456
|
await setOrganisationContextInSession(organisationId);
|
|
@@ -477,8 +514,13 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
477
514
|
secureParams.p_user_id = user.id;
|
|
478
515
|
}
|
|
479
516
|
|
|
480
|
-
// Add organisation_id parameter
|
|
481
|
-
|
|
517
|
+
// Add organisation_id parameter when needed
|
|
518
|
+
if (!bypassOrganisationFilter && organisationId) {
|
|
519
|
+
secureParams[paramName] = organisationId;
|
|
520
|
+
} else if (organisationId && !(paramName in params)) {
|
|
521
|
+
// Default to the current organisation if caller didn't specify one
|
|
522
|
+
secureParams[paramName] = organisationId;
|
|
523
|
+
}
|
|
482
524
|
|
|
483
525
|
// Add p_event_id if function needs it and event is selected
|
|
484
526
|
// CRITICAL: This must be added AFTER organisation_id but BEFORE caller params
|
|
@@ -505,7 +547,7 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
505
547
|
}
|
|
506
548
|
|
|
507
549
|
return data as T;
|
|
508
|
-
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, selectedEvent?.event_id, user?.id]);
|
|
550
|
+
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, selectedEvent?.event_id, user?.id, isSuperAdmin]);
|
|
509
551
|
|
|
510
552
|
// NEW: Phase 1 - Enhanced Security Features
|
|
511
553
|
const [dataAccessHistory, setDataAccessHistory] = useState<DataAccessRecord[]>([]);
|
|
@@ -570,12 +612,13 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
570
612
|
filters?: Record<string, any>
|
|
571
613
|
) => {
|
|
572
614
|
if (!isAuditLogEnabled || !user?.id) return;
|
|
573
|
-
|
|
615
|
+
const auditOrganisationId = getCurrentOrganisationId() || 'super-admin-bypass';
|
|
616
|
+
|
|
574
617
|
const record: DataAccessRecord = {
|
|
575
618
|
table,
|
|
576
619
|
operation,
|
|
577
620
|
userId: user.id,
|
|
578
|
-
organisationId:
|
|
621
|
+
organisationId: auditOrganisationId,
|
|
579
622
|
allowed,
|
|
580
623
|
timestamp: new Date().toISOString(),
|
|
581
624
|
query,
|
|
@@ -592,7 +635,7 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
592
635
|
table,
|
|
593
636
|
operation,
|
|
594
637
|
userId: user.id,
|
|
595
|
-
organisationId:
|
|
638
|
+
organisationId: auditOrganisationId,
|
|
596
639
|
timestamp: new Date().toISOString()
|
|
597
640
|
});
|
|
598
641
|
}
|
package/src/index.ts
CHANGED
|
@@ -85,7 +85,8 @@ export { Textarea } from './components/Textarea/Textarea';
|
|
|
85
85
|
export type { TextareaProps } from './components/Textarea/Textarea';
|
|
86
86
|
|
|
87
87
|
export { Alert, AlertTitle, AlertDescription } from './components/Alert/Alert';
|
|
88
|
-
export { Avatar
|
|
88
|
+
export { Avatar } from './components/Avatar/Avatar';
|
|
89
|
+
export type { AvatarProps } from './components/Avatar/Avatar';
|
|
89
90
|
|
|
90
91
|
export { Badge } from './components/Badge/Badge';
|
|
91
92
|
export type { BadgeProps, BadgeVariant } from './components/Badge/Badge';
|