@jmruthers/pace-core 0.5.191 → 0.6.1
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 +29 -0
- package/README.md +7 -1
- package/cursor-rules/00-pace-core-compliance.mdc +372 -0
- package/cursor-rules/01-standards-compliance.mdc +275 -0
- package/cursor-rules/02-project-structure.mdc +200 -0
- package/cursor-rules/03-solid-principles.mdc +341 -0
- package/cursor-rules/04-testing-standards.mdc +315 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +246 -0
- package/cursor-rules/06-code-quality.mdc +392 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +309 -0
- package/cursor-rules/CHANGELOG.md +101 -0
- package/cursor-rules/README.md +191 -0
- package/dist/{AuthService-CbP_utw2.d.ts → AuthService-DjnJHDtC.d.ts} +1 -0
- package/dist/{DataTable-Be6dH_dR.d.ts → DataTable-CH1U5Tpy.d.ts} +1 -1
- package/dist/{DataTable-WKRZD47S.js → DataTable-DQ7RSOHE.js} +8 -7
- package/dist/{PublicPageProvider-ULXC_u6U.d.ts → PublicPageProvider-ce4xlHYA.d.ts} +37 -156
- package/dist/{UnifiedAuthProvider-BYA9qB-o.d.ts → UnifiedAuthProvider-185Ih4dj.d.ts} +2 -0
- package/dist/{UnifiedAuthProvider-FTSG5XH7.js → UnifiedAuthProvider-ATAP5UTR.js} +3 -3
- package/dist/{api-IHKALJZD.js → api-N774RPUA.js} +2 -2
- package/dist/{chunk-6C4YBBJM.js → chunk-3QRJFVBR.js} +1 -1
- package/dist/chunk-3QRJFVBR.js.map +1 -0
- package/dist/{chunk-OETXORNB.js → chunk-3XTALGJF.js} +211 -136
- package/dist/chunk-3XTALGJF.js.map +1 -0
- package/dist/{chunk-6TQDD426.js → chunk-4N5C5XZU.js} +47 -228
- package/dist/chunk-4N5C5XZU.js.map +1 -0
- package/dist/{chunk-LOMZXPSN.js → chunk-4ZC4GX36.js} +47 -74
- package/dist/chunk-4ZC4GX36.js.map +1 -0
- package/dist/{chunk-6LTQQAT6.js → chunk-BYFSK72L.js} +357 -158
- package/dist/chunk-BYFSK72L.js.map +1 -0
- package/dist/{chunk-XYXSXPUK.js → chunk-EXUD6RNJ.js} +50 -10
- package/dist/chunk-EXUD6RNJ.js.map +1 -0
- package/dist/{chunk-VKB2CO4Z.js → chunk-GLK6VM3F.js} +244 -249
- package/dist/chunk-GLK6VM3F.js.map +1 -0
- package/dist/{chunk-HW3OVDUF.js → chunk-J36DSWQK.js} +1 -1
- package/dist/{chunk-HW3OVDUF.js.map → chunk-J36DSWQK.js.map} +1 -1
- package/dist/{chunk-XNYQOL3Z.js → chunk-JBKQ3SAO.js} +9 -18
- package/dist/chunk-JBKQ3SAO.js.map +1 -0
- package/dist/{chunk-ROXMHMY2.js → chunk-KNC55RTG.js} +13 -3
- package/dist/{chunk-ROXMHMY2.js.map → chunk-KNC55RTG.js.map} +1 -1
- package/dist/{chunk-QWWZ5CAQ.js → chunk-LXQLPRQ2.js} +2 -2
- package/dist/{chunk-ULHIJK66.js → chunk-T33XF5ZC.js} +255 -140
- package/dist/chunk-T33XF5ZC.js.map +1 -0
- package/dist/{chunk-VRGWKHDB.js → chunk-XM25TVIE.js} +100 -33
- package/dist/chunk-XM25TVIE.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +9 -9
- package/dist/hooks.d.ts +6 -6
- package/dist/hooks.js +20 -25
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +18 -21
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +2 -20
- package/dist/rbac/index.js +7 -9
- package/dist/{usePublicRouteParams-TZe0gy-4.d.ts → usePublicRouteParams-BJAlWfuJ.d.ts} +3 -3
- package/dist/{useToast-C8gR5ir4.d.ts → useToast-AyaT-x7p.d.ts} +2 -2
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +3 -3
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +2 -2
- package/docs/api/classes/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +10 -10
- package/docs/api/classes/StorageUtils.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/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 +24 -11
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/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 +2 -2
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +2 -2
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +4 -4
- package/docs/api/interfaces/ProgressProps.md +1 -1
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +2 -2
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +2 -2
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +2 -2
- 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 +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +2 -2
- package/docs/api/interfaces/RouteConfig.md +2 -2
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/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 +60 -38
- 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 +2 -2
- 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 +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +194 -209
- package/docs/getting-started/cursor-rules.md +262 -0
- package/docs/getting-started/installation-guide.md +6 -1
- package/docs/getting-started/quick-start.md +6 -1
- package/docs/migration/MIGRATION_GUIDE.md +4 -4
- package/docs/migration/REACT_19_MIGRATION.md +227 -0
- package/docs/migration/database-changes-december-2025.md +2 -1
- package/docs/rbac/event-based-apps.md +124 -6
- package/docs/standards/README.md +39 -0
- package/docs/troubleshooting/migration.md +4 -4
- package/examples/PublicPages/PublicEventPage.tsx +1 -1
- package/package.json +11 -6
- package/scripts/audit-consuming-app.cjs +961 -0
- package/scripts/check-pace-core-compliance.cjs +315 -61
- package/scripts/install-cursor-rules.cjs +236 -0
- package/src/__tests__/helpers/test-providers.tsx +1 -1
- package/src/__tests__/helpers/test-utils.tsx +1 -1
- package/src/__tests__/rls-policies.test.ts +3 -1
- package/src/components/Badge/Badge.tsx +2 -4
- package/src/components/Button/Button.tsx +5 -4
- package/src/components/Calendar/Calendar.tsx +1 -1
- package/src/components/DataTable/DataTable.test.tsx +57 -93
- package/src/components/DataTable/DataTable.tsx +2 -2
- package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +172 -45
- package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +121 -28
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +9 -8
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +20 -52
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +170 -34
- package/src/components/DataTable/__tests__/keyboard.test.tsx +75 -12
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +88 -16
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +12 -12
- package/src/components/DataTable/components/AccessDeniedPage.tsx +1 -1
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
- package/src/components/DataTable/components/DataTableCore.tsx +4 -7
- package/src/components/DataTable/components/DataTableModals.tsx +1 -1
- package/src/components/DataTable/components/EditableRow.tsx +1 -1
- package/src/components/DataTable/components/UnifiedTableBody.tsx +86 -17
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +23 -23
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +11 -11
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +36 -36
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +27 -27
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +39 -39
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +33 -33
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +29 -29
- package/src/components/DataTable/hooks/useColumnReordering.ts +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +75 -10
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +2 -2
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -14
- package/src/components/Dialog/Dialog.tsx +6 -5
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +1 -1
- package/src/components/EventSelector/EventSelector.tsx +1 -1
- package/src/components/FileDisplay/FileDisplay.test.tsx +4 -3
- package/src/components/FileDisplay/FileDisplay.tsx +16 -4
- package/src/components/Footer/Footer.tsx +1 -1
- package/src/components/Form/Form.test.tsx +36 -15
- package/src/components/Form/Form.tsx +30 -26
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +40 -40
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +1 -1
- package/src/components/Input/Input.tsx +28 -30
- package/src/components/Label/Label.tsx +1 -1
- package/src/components/LoadingSpinner/LoadingSpinner.tsx +1 -1
- package/src/components/LoginForm/LoginForm.test.tsx +42 -42
- package/src/components/LoginForm/LoginForm.tsx +8 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +6 -4
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -11
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -1
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +1 -1
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +75 -52
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +98 -69
- package/src/components/PaceAppLayout/README.md +1 -1
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -8
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +33 -33
- package/src/components/PasswordChange/PasswordChangeForm.tsx +1 -1
- package/src/components/Progress/Progress.tsx +1 -1
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +5 -9
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +0 -1
- package/src/components/PublicLayout/PublicPageLayout.tsx +1 -1
- package/src/components/PublicLayout/PublicPageProvider.tsx +0 -1
- package/src/components/Select/Select.tsx +33 -22
- package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +1 -1
- package/src/components/Table/Table.tsx +1 -1
- package/src/components/Textarea/Textarea.tsx +27 -29
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/components/Tooltip/Tooltip.tsx +1 -1
- package/src/components/UserMenu/UserMenu.tsx +1 -1
- package/src/hooks/__tests__/hooks.integration.test.tsx +80 -55
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +14 -7
- package/src/hooks/__tests__/useStorage.unit.test.ts +36 -36
- package/src/hooks/public/usePublicEvent.ts +1 -1
- package/src/hooks/public/usePublicEventLogo.ts +1 -1
- package/src/hooks/public/usePublicRouteParams.ts +1 -1
- package/src/hooks/services/useAuthService.ts +21 -3
- package/src/hooks/services/useEventService.ts +21 -3
- package/src/hooks/services/useInactivityService.ts +21 -3
- package/src/hooks/services/useOrganisationService.ts +21 -3
- package/src/hooks/useDataTableState.ts +8 -18
- package/src/hooks/useFileDisplay.ts +10 -17
- package/src/hooks/useFocusManagement.ts +2 -2
- package/src/hooks/useFocusTrap.ts +4 -4
- package/src/hooks/useFormDialog.ts +8 -7
- package/src/hooks/useInactivityTracker.ts +1 -1
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/hooks/useSecureDataAccess.test.ts +16 -9
- package/src/hooks/useSecureDataAccess.ts +22 -6
- package/src/hooks/useToast.ts +2 -2
- package/src/providers/__tests__/OrganisationProvider.test.tsx +57 -13
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +21 -6
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +10 -10
- package/src/providers/services/EventServiceProvider.tsx +0 -8
- package/src/providers/services/UnifiedAuthProvider.tsx +196 -46
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +13 -3
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +34 -40
- package/src/rbac/__tests__/isSuperAdmin.real.test.ts +82 -0
- package/src/rbac/adapters.tsx +3 -22
- package/src/rbac/api.test.ts +2 -2
- package/src/rbac/api.ts +7 -1
- package/src/rbac/components/EnhancedNavigationMenu.tsx +3 -16
- package/src/rbac/components/NavigationGuard.tsx +2 -11
- package/src/rbac/components/NavigationProvider.tsx +1 -2
- package/src/rbac/components/PagePermissionGuard.tsx +1 -1
- package/src/rbac/components/PagePermissionProvider.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +46 -13
- package/src/rbac/components/RoleBasedRouter.tsx +1 -1
- package/src/rbac/components/SecureDataProvider.tsx +1 -2
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +7 -43
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +4 -11
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +3 -3
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +1 -1
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +1 -1
- package/src/rbac/engine.ts +14 -2
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/usePermissions.ts +51 -11
- package/src/rbac/hooks/useRBAC.ts +3 -13
- package/src/rbac/hooks/useResolvedScope.test.ts +75 -54
- package/src/rbac/hooks/useResolvedScope.ts +58 -33
- package/src/rbac/hooks/useSecureSupabase.ts +4 -9
- package/src/rbac/secureClient.ts +43 -0
- package/src/services/EventService.ts +4 -57
- package/src/services/InactivityService.ts +127 -34
- package/src/services/OrganisationService.ts +68 -10
- package/src/utils/security/secureDataAccess.test.ts +31 -20
- package/src/utils/security/secureDataAccess.ts +4 -3
- package/dist/chunk-6C4YBBJM.js.map +0 -1
- package/dist/chunk-6LTQQAT6.js.map +0 -1
- package/dist/chunk-6TQDD426.js.map +0 -1
- package/dist/chunk-LOMZXPSN.js.map +0 -1
- package/dist/chunk-OETXORNB.js.map +0 -1
- package/dist/chunk-ULHIJK66.js.map +0 -1
- package/dist/chunk-VKB2CO4Z.js.map +0 -1
- package/dist/chunk-VRGWKHDB.js.map +0 -1
- package/dist/chunk-XNYQOL3Z.js.map +0 -1
- package/dist/chunk-XYXSXPUK.js.map +0 -1
- package/scripts/check-pace-core-compliance.js +0 -512
- package/src/rbac/hooks/useSuperAdminBypass.ts +0 -126
- package/src/utils/context/superAdminOverride.ts +0 -58
- /package/dist/{DataTable-WKRZD47S.js.map → DataTable-DQ7RSOHE.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-FTSG5XH7.js.map → UnifiedAuthProvider-ATAP5UTR.js.map} +0 -0
- /package/dist/{api-IHKALJZD.js.map → api-N774RPUA.js.map} +0 -0
- /package/dist/{chunk-QWWZ5CAQ.js.map → chunk-LXQLPRQ2.js.map} +0 -0
- /package/examples/{rbac → RBAC}/CompleteRBACExample.tsx +0 -0
- /package/examples/{rbac → RBAC}/EventBasedApp.tsx +0 -0
- /package/examples/{rbac → RBAC}/PermissionExample.tsx +0 -0
- /package/examples/{rbac → RBAC}/index.ts +0 -0
|
@@ -116,10 +116,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
116
116
|
if (user?.id) {
|
|
117
117
|
try {
|
|
118
118
|
this.isSuperAdmin = await isSuperAdmin(user.id as UUID);
|
|
119
|
-
logger.debug('EventService', 'Updated super admin status', {
|
|
120
|
-
userId: user.id,
|
|
121
|
-
isSuperAdmin: this.isSuperAdmin
|
|
122
|
-
});
|
|
123
119
|
} catch (error) {
|
|
124
120
|
logger.warn('EventService', 'Failed to check super admin status', { error });
|
|
125
121
|
this.isSuperAdmin = false; // Default to false on error
|
|
@@ -151,9 +147,10 @@ export class EventService extends BaseService implements IEventService {
|
|
|
151
147
|
|
|
152
148
|
// Event state getters
|
|
153
149
|
getEvents(): Event[] {
|
|
154
|
-
// Return
|
|
155
|
-
// This
|
|
156
|
-
|
|
150
|
+
// Return stable array reference - only create new array when events actually change
|
|
151
|
+
// This prevents unnecessary re-renders when getEvents() is called on every render
|
|
152
|
+
// The service's notify() mechanism handles change detection
|
|
153
|
+
return this.events;
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
getSelectedEvent(): Event | null {
|
|
@@ -308,12 +305,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
308
305
|
async initialize(): Promise<void> {
|
|
309
306
|
// Only call super.initialize() which will call doInitialize() and fetchEvents()
|
|
310
307
|
// Don't call fetchEvents() again here to avoid double-fetching
|
|
311
|
-
logger.debug('EventService', 'initialize() called', {
|
|
312
|
-
isInitializedRef: this.isInitializedRef,
|
|
313
|
-
hasUser: !!this.user,
|
|
314
|
-
hasSession: !!this.session,
|
|
315
|
-
appName: this.appName
|
|
316
|
-
});
|
|
317
308
|
await super.initialize();
|
|
318
309
|
}
|
|
319
310
|
|
|
@@ -322,23 +313,13 @@ export class EventService extends BaseService implements IEventService {
|
|
|
322
313
|
}
|
|
323
314
|
|
|
324
315
|
protected async doInitialize(): Promise<void> {
|
|
325
|
-
logger.debug('EventService', 'doInitialize() called', {
|
|
326
|
-
isInitializedRef: this.isInitializedRef,
|
|
327
|
-
isFetchingRef: this.isFetchingRef,
|
|
328
|
-
hasUser: !!this.user,
|
|
329
|
-
hasSession: !!this.session,
|
|
330
|
-
appName: this.appName
|
|
331
|
-
});
|
|
332
|
-
|
|
333
316
|
// Skip if already initialized
|
|
334
317
|
if (this.isInitializedRef) {
|
|
335
|
-
logger.debug('EventService', 'Skipping initialization - already initialized');
|
|
336
318
|
return;
|
|
337
319
|
}
|
|
338
320
|
|
|
339
321
|
// Skip if already fetching
|
|
340
322
|
if (this.isFetchingRef) {
|
|
341
|
-
logger.debug('EventService', 'Skipping initialization - already fetching');
|
|
342
323
|
return;
|
|
343
324
|
}
|
|
344
325
|
|
|
@@ -355,16 +336,9 @@ export class EventService extends BaseService implements IEventService {
|
|
|
355
336
|
// For event-required apps, selectedOrganisation may be null (org derived from event)
|
|
356
337
|
// For org-required apps, selectedOrganisation is required
|
|
357
338
|
if (!this.user) {
|
|
358
|
-
logger.debug('EventService', 'Skipping initialization - missing user');
|
|
359
339
|
return;
|
|
360
340
|
}
|
|
361
341
|
|
|
362
|
-
logger.debug('EventService', 'Initializing - fetching events', {
|
|
363
|
-
userId: this.user.id,
|
|
364
|
-
organisationId: this.selectedOrganisation?.id || 'derived-from-event',
|
|
365
|
-
appName: this.appName
|
|
366
|
-
});
|
|
367
|
-
|
|
368
342
|
// Initial setup - fetch events on initialization
|
|
369
343
|
await this.fetchEvents(false);
|
|
370
344
|
|
|
@@ -380,12 +354,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
380
354
|
// For event-required apps, selectedOrganisation may be null (org derived from event)
|
|
381
355
|
// For org-required apps, selectedOrganisation is required
|
|
382
356
|
if (!this.user || !this.session || !this.supabaseClient || !this.appName) {
|
|
383
|
-
logger.debug('EventService', 'Skipping fetchEvents - missing dependencies', {
|
|
384
|
-
hasUser: !!this.user,
|
|
385
|
-
hasSession: !!this.session,
|
|
386
|
-
hasSupabaseClient: !!this.supabaseClient,
|
|
387
|
-
appName: this.appName
|
|
388
|
-
});
|
|
389
357
|
// Already false from initialization, just notify
|
|
390
358
|
this.notify();
|
|
391
359
|
return;
|
|
@@ -397,7 +365,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
397
365
|
|
|
398
366
|
// Prevent multiple simultaneous fetches
|
|
399
367
|
if (this.isFetchingRef) {
|
|
400
|
-
logger.debug('EventService', 'Skipping fetchEvents - already fetching');
|
|
401
368
|
return;
|
|
402
369
|
}
|
|
403
370
|
|
|
@@ -431,9 +398,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
431
398
|
if (userIsSuperAdmin) {
|
|
432
399
|
// Super admin: Pass null to see all events across all organisations
|
|
433
400
|
organisationIdForRpc = null;
|
|
434
|
-
logger.debug('EventService', 'Super admin detected - fetching all events', {
|
|
435
|
-
userId: this.user.id
|
|
436
|
-
});
|
|
437
401
|
} else {
|
|
438
402
|
// Not super admin: determine org from context based on app type
|
|
439
403
|
if (this.selectedEvent) {
|
|
@@ -443,10 +407,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
443
407
|
// Event-required app with no selected event yet: pass null to get all accessible events
|
|
444
408
|
// The RPC will filter by event app roles, returning all events the user has access to
|
|
445
409
|
organisationIdForRpc = null;
|
|
446
|
-
logger.debug('EventService', 'Event-required app: fetching all accessible events (no event selected yet)', {
|
|
447
|
-
userId: this.user.id,
|
|
448
|
-
appName: this.appName
|
|
449
|
-
});
|
|
450
410
|
} else if (this.selectedOrganisation) {
|
|
451
411
|
// Org-required app: use selected organisation
|
|
452
412
|
organisationIdForRpc = this.selectedOrganisation.id;
|
|
@@ -476,12 +436,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
476
436
|
}
|
|
477
437
|
}
|
|
478
438
|
|
|
479
|
-
logger.debug('EventService', 'Fetching events via RPC', {
|
|
480
|
-
userId: this.user.id,
|
|
481
|
-
organisationId: organisationIdForRpc,
|
|
482
|
-
appName: this.appName
|
|
483
|
-
});
|
|
484
|
-
|
|
485
439
|
// Call the RPC function following the established pattern
|
|
486
440
|
// For super admins, pass null for p_organisation_id to see all events
|
|
487
441
|
let { data, error: rpcError } = await this.supabaseClient.rpc('data_user_events_get', {
|
|
@@ -490,13 +444,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
490
444
|
p_app_name: this.appName
|
|
491
445
|
});
|
|
492
446
|
|
|
493
|
-
logger.debug('EventService', 'RPC response received', {
|
|
494
|
-
hasData: !!data,
|
|
495
|
-
dataLength: Array.isArray(data) ? data.length : 'not array',
|
|
496
|
-
hasError: !!rpcError,
|
|
497
|
-
error: rpcError
|
|
498
|
-
});
|
|
499
|
-
|
|
500
447
|
if (rpcError) {
|
|
501
448
|
logger.error('EventService', 'RPC error fetching events:', rpcError);
|
|
502
449
|
throw new Error(rpcError.message || 'Failed to fetch events');
|
|
@@ -145,12 +145,31 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
145
145
|
if (this.inactivityTracker) {
|
|
146
146
|
this.inactivityTracker.resetActivity();
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
// Store previous values
|
|
150
|
+
const prevIsIdle = this._isIdle;
|
|
151
|
+
const prevShowWarning = this._showWarning;
|
|
152
|
+
const prevShowInactivityWarning = this._showInactivityWarning;
|
|
153
|
+
const prevInactivityTimeRemaining = this._inactivityTimeRemaining;
|
|
154
|
+
const prevTimeRemaining = this._timeRemaining;
|
|
155
|
+
|
|
156
|
+
// Update state
|
|
148
157
|
this._isIdle = false;
|
|
149
158
|
this._showWarning = false;
|
|
150
159
|
this._showInactivityWarning = false;
|
|
151
160
|
this._inactivityTimeRemaining = 0;
|
|
152
161
|
this._timeRemaining = this.idleTimeoutMs;
|
|
153
|
-
|
|
162
|
+
|
|
163
|
+
// Only notify if values actually changed
|
|
164
|
+
if (
|
|
165
|
+
prevIsIdle !== this._isIdle ||
|
|
166
|
+
prevShowWarning !== this._showWarning ||
|
|
167
|
+
prevShowInactivityWarning !== this._showInactivityWarning ||
|
|
168
|
+
prevInactivityTimeRemaining !== this._inactivityTimeRemaining ||
|
|
169
|
+
prevTimeRemaining !== this._timeRemaining
|
|
170
|
+
) {
|
|
171
|
+
this.notify();
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
startTracking(): void {
|
|
@@ -275,10 +294,63 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
275
294
|
let idleTimer: NodeJS.Timeout | null = null;
|
|
276
295
|
let warningTimer: NodeJS.Timeout | null = null;
|
|
277
296
|
let lastActivity = Date.now();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
297
|
+
let pollInterval: NodeJS.Timeout | null = null;
|
|
298
|
+
|
|
299
|
+
// Store previous state for comparison
|
|
300
|
+
let prevIsIdle = false;
|
|
301
|
+
let prevShowWarning = false;
|
|
302
|
+
let prevShowInactivityWarning = false;
|
|
303
|
+
let prevInactivityTimeRemaining = 0;
|
|
304
|
+
let prevTimeRemaining = this.idleTimeoutMs;
|
|
305
|
+
|
|
306
|
+
// Poll every 10 seconds to check for state changes
|
|
307
|
+
const pollInactivityState = () => {
|
|
308
|
+
const now = Date.now();
|
|
309
|
+
const timeSinceActivity = now - lastActivity;
|
|
310
|
+
const timeUntilIdle = this.idleTimeoutMs - timeSinceActivity;
|
|
311
|
+
const timeUntilWarning = (this.idleTimeoutMs - this.warnBeforeMs) - timeSinceActivity;
|
|
312
|
+
|
|
313
|
+
// Calculate new state values based on time since last activity
|
|
314
|
+
const newIsIdle = timeSinceActivity >= (this.idleTimeoutMs - this.warnBeforeMs);
|
|
315
|
+
const newShowWarning = newIsIdle && timeSinceActivity < this.idleTimeoutMs;
|
|
316
|
+
const newShowInactivityWarning = newShowWarning;
|
|
317
|
+
const newInactivityTimeRemaining = newShowWarning
|
|
318
|
+
? Math.ceil((this.idleTimeoutMs - timeSinceActivity) / 1000)
|
|
319
|
+
: 0;
|
|
320
|
+
const newTimeRemaining = Math.max(0, timeUntilIdle);
|
|
321
|
+
|
|
322
|
+
// Check if state actually changed
|
|
323
|
+
const stateChanged =
|
|
324
|
+
prevIsIdle !== newIsIdle ||
|
|
325
|
+
prevShowWarning !== newShowWarning ||
|
|
326
|
+
prevShowInactivityWarning !== newShowInactivityWarning ||
|
|
327
|
+
prevInactivityTimeRemaining !== newInactivityTimeRemaining ||
|
|
328
|
+
prevTimeRemaining !== newTimeRemaining;
|
|
329
|
+
|
|
330
|
+
// Only update and notify if state changed
|
|
331
|
+
if (stateChanged) {
|
|
332
|
+
this._isIdle = newIsIdle;
|
|
333
|
+
this._showWarning = newShowWarning;
|
|
334
|
+
this._showInactivityWarning = newShowInactivityWarning;
|
|
335
|
+
this._inactivityTimeRemaining = newInactivityTimeRemaining;
|
|
336
|
+
this._timeRemaining = newTimeRemaining;
|
|
337
|
+
|
|
338
|
+
// Update previous state
|
|
339
|
+
prevIsIdle = newIsIdle;
|
|
340
|
+
prevShowWarning = newShowWarning;
|
|
341
|
+
prevShowInactivityWarning = newShowInactivityWarning;
|
|
342
|
+
prevInactivityTimeRemaining = newInactivityTimeRemaining;
|
|
343
|
+
prevTimeRemaining = newTimeRemaining;
|
|
344
|
+
|
|
345
|
+
this.notify();
|
|
346
|
+
|
|
347
|
+
// Handle idle logout if needed
|
|
348
|
+
if (newIsIdle && timeSinceActivity >= this.idleTimeoutMs) {
|
|
349
|
+
this.handleIdleLogout();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
281
352
|
|
|
353
|
+
// Update timers based on current state
|
|
282
354
|
if (idleTimer) {
|
|
283
355
|
clearTimeout(idleTimer);
|
|
284
356
|
idleTimer = null;
|
|
@@ -288,47 +360,58 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
288
360
|
clearTimeout(warningTimer);
|
|
289
361
|
warningTimer = null;
|
|
290
362
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const startIdleTimer = () => {
|
|
300
|
-
if (idleTimer) {
|
|
301
|
-
clearTimeout(idleTimer);
|
|
363
|
+
|
|
364
|
+
// Set up timers for next state transitions
|
|
365
|
+
if (!newIsIdle && timeUntilIdle > 0) {
|
|
366
|
+
idleTimer = setTimeout(() => {
|
|
367
|
+
pollInactivityState();
|
|
368
|
+
}, Math.min(10000, timeUntilIdle));
|
|
302
369
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
this._isIdle = true;
|
|
306
|
-
this._showWarning = true;
|
|
307
|
-
this.notify();
|
|
308
|
-
|
|
309
|
-
// Start warning timer
|
|
370
|
+
|
|
371
|
+
if (newShowWarning && timeUntilIdle > 0) {
|
|
310
372
|
warningTimer = setTimeout(() => {
|
|
311
373
|
this.handleIdleLogout();
|
|
312
|
-
},
|
|
313
|
-
}
|
|
374
|
+
}, timeUntilIdle);
|
|
375
|
+
}
|
|
314
376
|
};
|
|
315
377
|
|
|
316
|
-
const
|
|
378
|
+
const resetTimers = () => {
|
|
379
|
+
// Only update lastActivity - don't notify yet
|
|
380
|
+
lastActivity = Date.now();
|
|
381
|
+
|
|
382
|
+
// Clear timers
|
|
383
|
+
if (idleTimer) {
|
|
384
|
+
clearTimeout(idleTimer);
|
|
385
|
+
idleTimer = null;
|
|
386
|
+
}
|
|
387
|
+
|
|
317
388
|
if (warningTimer) {
|
|
318
389
|
clearTimeout(warningTimer);
|
|
390
|
+
warningTimer = null;
|
|
319
391
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
392
|
+
|
|
393
|
+
// Reset state values (will be checked on next poll)
|
|
394
|
+
this._showInactivityWarning = false;
|
|
395
|
+
this._inactivityTimeRemaining = 0;
|
|
396
|
+
this._isIdle = false;
|
|
397
|
+
this._showWarning = false;
|
|
398
|
+
|
|
399
|
+
// Update previous state to match
|
|
400
|
+
prevIsIdle = false;
|
|
401
|
+
prevShowWarning = false;
|
|
402
|
+
prevShowInactivityWarning = false;
|
|
403
|
+
prevInactivityTimeRemaining = 0;
|
|
404
|
+
prevTimeRemaining = this.idleTimeoutMs;
|
|
405
|
+
|
|
406
|
+
// Poll will check and notify if needed
|
|
324
407
|
};
|
|
325
408
|
|
|
326
|
-
// Activity detection
|
|
409
|
+
// Activity detection - only updates lastActivity, doesn't notify
|
|
327
410
|
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
|
|
328
411
|
|
|
329
412
|
const handleActivity = () => {
|
|
330
413
|
resetTimers();
|
|
331
|
-
|
|
414
|
+
// Don't call notify - polling will handle state updates
|
|
332
415
|
};
|
|
333
416
|
|
|
334
417
|
// Add event listeners
|
|
@@ -336,8 +419,13 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
336
419
|
document.addEventListener(event, handleActivity, true);
|
|
337
420
|
});
|
|
338
421
|
|
|
339
|
-
// Start
|
|
340
|
-
|
|
422
|
+
// Start polling every 10 seconds
|
|
423
|
+
pollInterval = setInterval(() => {
|
|
424
|
+
pollInactivityState();
|
|
425
|
+
}, 10000); // 10 seconds
|
|
426
|
+
|
|
427
|
+
// Initial poll
|
|
428
|
+
pollInactivityState();
|
|
341
429
|
|
|
342
430
|
// Store cleanup function
|
|
343
431
|
this.cleanupHandlers = () => {
|
|
@@ -350,6 +438,11 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
350
438
|
clearTimeout(warningTimer);
|
|
351
439
|
warningTimer = null;
|
|
352
440
|
}
|
|
441
|
+
|
|
442
|
+
if (pollInterval) {
|
|
443
|
+
clearInterval(pollInterval);
|
|
444
|
+
pollInterval = null;
|
|
445
|
+
}
|
|
353
446
|
|
|
354
447
|
activityEvents.forEach(event => {
|
|
355
448
|
document.removeEventListener(event, handleActivity, true);
|
|
@@ -364,6 +457,6 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
364
457
|
};
|
|
365
458
|
|
|
366
459
|
this._isTracking = true;
|
|
367
|
-
this.notify();
|
|
460
|
+
this.notify(); // Initial notification only
|
|
368
461
|
}
|
|
369
462
|
}
|
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
import { setOrganisationContext } from '../utils/context/organisationContext';
|
|
21
21
|
import { logger } from '../utils/core/logger';
|
|
22
22
|
import { assertUserId, assertOrganisationId } from '../types/core';
|
|
23
|
+
import { isSuperAdmin } from '../rbac/api';
|
|
24
|
+
import type { UUID } from '../rbac/types';
|
|
23
25
|
|
|
24
26
|
// Type for RPC response from data_user_organisation_roles_get
|
|
25
27
|
interface OrganisationRoleRpcResponse {
|
|
@@ -46,6 +48,7 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
46
48
|
private _roleMapState: Map<string, string> = new Map();
|
|
47
49
|
private _isLoading = false;
|
|
48
50
|
private _error: Error | null = null;
|
|
51
|
+
private _isSuperAdmin: boolean = false; // Cache super admin status
|
|
49
52
|
private _isContextReady = false;
|
|
50
53
|
private retryCount = 0;
|
|
51
54
|
|
|
@@ -81,17 +84,21 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
81
84
|
// Additional methods for testing
|
|
82
85
|
setSelectedOrganisation(organisation: Organisation | null): void {
|
|
83
86
|
// SECURITY: Validate organisation is in user's accessible organisations (only if orgs are loaded)
|
|
87
|
+
// Exception: Super admins can set any organisation (they have global access)
|
|
84
88
|
if (organisation && this._organisations.length > 0) {
|
|
85
|
-
|
|
86
|
-
if (!
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
// Only validate if user is not a super admin (use cached status)
|
|
90
|
+
if (!this._isSuperAdmin) {
|
|
91
|
+
const isValidOrg = this._organisations.some(org => org.id === organisation.id);
|
|
92
|
+
if (!isValidOrg) {
|
|
93
|
+
logger.warn('OrganisationService', 'Attempted to set invalid organisation - not in user\'s accessible organisations', {
|
|
94
|
+
organisationId: organisation.id,
|
|
95
|
+
organisationName: organisation.name,
|
|
96
|
+
accessibleOrgIds: this._organisations.map(o => o.id)
|
|
97
|
+
});
|
|
98
|
+
// Don't set invalid organisation - this prevents security issues
|
|
99
|
+
// If organisations haven't loaded yet, validation will happen in loadUserOrganisations()
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
|
|
@@ -140,6 +147,11 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
140
147
|
const wasAuthenticated = !!(this.user && this.session);
|
|
141
148
|
const isAuthenticated = !!(user && session);
|
|
142
149
|
|
|
150
|
+
// Reset super admin cache when user changes
|
|
151
|
+
if (this.user?.id !== user?.id) {
|
|
152
|
+
this._isSuperAdmin = false;
|
|
153
|
+
}
|
|
154
|
+
|
|
143
155
|
this.user = user;
|
|
144
156
|
this.session = session;
|
|
145
157
|
|
|
@@ -460,11 +472,47 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
460
472
|
throw membershipError;
|
|
461
473
|
}
|
|
462
474
|
|
|
475
|
+
// Check if user is super admin - super admins don't need organisation memberships
|
|
476
|
+
let userIsSuperAdmin = false;
|
|
477
|
+
if (this.user?.id) {
|
|
478
|
+
try {
|
|
479
|
+
userIsSuperAdmin = await isSuperAdmin(this.user.id as UUID);
|
|
480
|
+
this._isSuperAdmin = userIsSuperAdmin; // Cache the result
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logger.warn('OrganisationService', 'Failed to check super admin status', { error });
|
|
483
|
+
// Continue with normal flow if check fails
|
|
484
|
+
this._isSuperAdmin = false;
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
this._isSuperAdmin = false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Super admins can proceed without organisation memberships
|
|
463
491
|
if (!memberships || memberships.length === 0) {
|
|
492
|
+
if (userIsSuperAdmin) {
|
|
493
|
+
// Super admin without org memberships - allow empty state
|
|
494
|
+
this._organisations = [];
|
|
495
|
+
this._userMemberships = [];
|
|
496
|
+
this._isLoading = false;
|
|
497
|
+
this._error = null;
|
|
498
|
+
this._isContextReady = true;
|
|
499
|
+
this.notify();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
464
502
|
throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
|
|
465
503
|
}
|
|
466
504
|
|
|
467
505
|
if (!organisations || organisations.length === 0) {
|
|
506
|
+
if (userIsSuperAdmin) {
|
|
507
|
+
// Super admin without orgs - allow empty state
|
|
508
|
+
this._organisations = [];
|
|
509
|
+
this._userMemberships = [];
|
|
510
|
+
this._isLoading = false;
|
|
511
|
+
this._error = null;
|
|
512
|
+
this._isContextReady = true;
|
|
513
|
+
this.notify();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
468
516
|
throw new Error('No organisations found in role data') as OrganisationSecurityError;
|
|
469
517
|
}
|
|
470
518
|
|
|
@@ -481,6 +529,16 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
481
529
|
// Filter to active organisations only
|
|
482
530
|
|
|
483
531
|
if (activeOrgs.length === 0) {
|
|
532
|
+
if (userIsSuperAdmin) {
|
|
533
|
+
// Super admin without active orgs - allow empty state
|
|
534
|
+
this._organisations = [];
|
|
535
|
+
this._userMemberships = [];
|
|
536
|
+
this._isLoading = false;
|
|
537
|
+
this._error = null;
|
|
538
|
+
this._isContextReady = true;
|
|
539
|
+
this.notify();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
484
542
|
throw new Error('User has no access to active organisations') as OrganisationSecurityError;
|
|
485
543
|
}
|
|
486
544
|
|
|
@@ -21,15 +21,25 @@ const mockSupabaseClient = {
|
|
|
21
21
|
let mockQueryBuilder: any;
|
|
22
22
|
|
|
23
23
|
const createMockQueryBuilder = (customData?: any, customError?: any) => {
|
|
24
|
+
// Create spy functions that return this for chaining
|
|
25
|
+
const selectSpy = vi.fn(function(this: any) { return this; });
|
|
26
|
+
const eqSpy = vi.fn(function(this: any) { return this; });
|
|
27
|
+
const orderSpy = vi.fn(function(this: any) { return this; });
|
|
28
|
+
const limitSpy = vi.fn(function(this: any) { return this; });
|
|
29
|
+
const rangeSpy = vi.fn(function(this: any) { return this; });
|
|
30
|
+
const insertSpy = vi.fn(function(this: any) { return this; });
|
|
31
|
+
const updateSpy = vi.fn(function(this: any) { return this; });
|
|
32
|
+
const deleteSpy = vi.fn(function(this: any) { return this; });
|
|
33
|
+
|
|
24
34
|
const builder: any = {
|
|
25
|
-
select:
|
|
26
|
-
eq:
|
|
27
|
-
order:
|
|
28
|
-
limit:
|
|
29
|
-
range:
|
|
30
|
-
insert:
|
|
31
|
-
update:
|
|
32
|
-
delete:
|
|
35
|
+
select: selectSpy,
|
|
36
|
+
eq: eqSpy,
|
|
37
|
+
order: orderSpy,
|
|
38
|
+
limit: limitSpy,
|
|
39
|
+
range: rangeSpy,
|
|
40
|
+
insert: insertSpy,
|
|
41
|
+
update: updateSpy,
|
|
42
|
+
delete: deleteSpy,
|
|
33
43
|
single: vi.fn().mockImplementation(() => {
|
|
34
44
|
const data = customData !== undefined ? customData : { id: 'test-123' };
|
|
35
45
|
const error = customError !== undefined ? customError : null;
|
|
@@ -56,7 +66,7 @@ const createMockQueryBuilder = (customData?: any, customError?: any) => {
|
|
|
56
66
|
describe('secureDataAccess', () => {
|
|
57
67
|
let secureDataAccess: ReturnType<typeof createSecureDataAccess>;
|
|
58
68
|
const mockOrganisationId = 'org-123';
|
|
59
|
-
const mockTable = '
|
|
69
|
+
const mockTable = 'core_events'; // Use a table that has organisation_id column (pace_person doesn't have it)
|
|
60
70
|
const mockSelect = 'id, name, organisation_id';
|
|
61
71
|
|
|
62
72
|
beforeEach(() => {
|
|
@@ -332,10 +342,10 @@ describe('secureDataAccess', () => {
|
|
|
332
342
|
const mockFilters = { id: '1' };
|
|
333
343
|
const mockResult = { id: '1', name: 'Updated Item', organisation_id: mockOrganisationId };
|
|
334
344
|
|
|
335
|
-
|
|
336
|
-
mockQueryBuilder
|
|
337
|
-
mockQueryBuilder.select = vi.fn().mockReturnThis();
|
|
345
|
+
// Create a fresh mock query builder with proper chaining
|
|
346
|
+
mockQueryBuilder = createMockQueryBuilder(mockResult);
|
|
338
347
|
mockQueryBuilder.single = vi.fn().mockResolvedValue({ data: mockResult, error: null });
|
|
348
|
+
mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
|
|
339
349
|
|
|
340
350
|
const result = await secureDataAccess.secureUpdate(mockTable, mockData, mockFilters, mockOrganisationId);
|
|
341
351
|
|
|
@@ -366,9 +376,13 @@ describe('secureDataAccess', () => {
|
|
|
366
376
|
it('should delete data with organisation filter', async () => {
|
|
367
377
|
const mockFilters = { id: '1' };
|
|
368
378
|
|
|
369
|
-
|
|
370
|
-
mockQueryBuilder
|
|
371
|
-
|
|
379
|
+
// Create a fresh mock query builder with proper chaining
|
|
380
|
+
mockQueryBuilder = createMockQueryBuilder();
|
|
381
|
+
// Make the query builder thenable and resolve successfully
|
|
382
|
+
mockQueryBuilder.then = function(resolve: any) {
|
|
383
|
+
return Promise.resolve({ data: null, error: null }).then(resolve);
|
|
384
|
+
};
|
|
385
|
+
mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
|
|
372
386
|
|
|
373
387
|
const result = await secureDataAccess.secureDelete(mockTable, mockFilters, mockOrganisationId);
|
|
374
388
|
|
|
@@ -687,12 +701,9 @@ describe('secureDataAccess', () => {
|
|
|
687
701
|
};
|
|
688
702
|
|
|
689
703
|
const mockData = [{ id: '1', name: 'Test', organisation_id: mockOrganisationId }];
|
|
690
|
-
// Create a new mock with the expected data
|
|
691
|
-
mockQueryBuilder = createMockQueryBuilder(mockData);
|
|
692
|
-
mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
|
|
693
704
|
// Create a new mock with the expected data
|
|
694
|
-
|
|
695
|
-
|
|
705
|
+
mockQueryBuilder = createMockQueryBuilder(mockData);
|
|
706
|
+
mockSupabaseClient.from = vi.fn().mockReturnValue(mockQueryBuilder);
|
|
696
707
|
|
|
697
708
|
const options: SecureQueryOptions = {
|
|
698
709
|
table: mockTable,
|
|
@@ -99,9 +99,10 @@ export const createSecureDataAccess = (
|
|
|
99
99
|
// SECURITY: Phase 2 additions - complete organisation table mapping
|
|
100
100
|
'organisation_audit_log', 'organisation_invitations', 'organisation_app_access',
|
|
101
101
|
// SECURITY: Emergency additions for Phase 1 fixes
|
|
102
|
-
'cake_meal', 'cake_mealtype',
|
|
103
|
-
// NOTE: core_member, medi_profile, core_contact, core_consent,
|
|
104
|
-
// are now person-scoped (not organisation-scoped)
|
|
102
|
+
'cake_meal', 'cake_mealtype',
|
|
103
|
+
// NOTE: core_person, pace_person, core_member, medi_profile, core_contact, core_consent,
|
|
104
|
+
// core_identification, core_qualification are now person-scoped (not organisation-scoped)
|
|
105
|
+
// and do NOT have organisation_id columns - removed from this list
|
|
105
106
|
// SECURITY: Phase 3A additions - medical and personal data
|
|
106
107
|
// NOTE: medi_condition, medi_diet, medi_action_plan, medi_profile_versions are now person-scoped
|
|
107
108
|
// (via medi_profile) - removed from this list
|