@jmruthers/pace-core 0.5.189 → 0.5.191
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-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
- package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
- package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
- package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
- package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
- package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
- package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
- package/dist/chunk-6LTQQAT6.js.map +1 -0
- package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
- package/dist/chunk-6TQDD426.js.map +1 -0
- package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
- package/dist/chunk-G37KK66H.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-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
- package/dist/chunk-LOMZXPSN.js.map +1 -0
- package/dist/chunk-OETXORNB.js +614 -0
- package/dist/chunk-OETXORNB.js.map +1 -0
- package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
- package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
- package/dist/chunk-ROXMHMY2.js.map +1 -0
- package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
- package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
- package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
- package/dist/chunk-VKB2CO4Z.js.map +1 -0
- package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
- package/dist/chunk-VRGWKHDB.js.map +1 -0
- package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
- package/dist/chunk-XNYQOL3Z.js.map +1 -0
- package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
- package/dist/chunk-XYXSXPUK.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 +5 -6
- package/dist/components.js +19 -19
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
- 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 +20 -15
- package/dist/hooks.js +14 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.js +86 -81
- 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 +77 -13
- package/dist/rbac/index.js +12 -9
- package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
- package/dist/utils.d.ts +8 -8
- 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 +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +2 -2
- package/docs/api/classes/SecureSupabaseClient.md +25 -20
- 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 +1 -1
- 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 +2 -2
- 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 +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 +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 +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 +5 -5
- 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 +165 -106
- package/docs/api-reference/components.md +15 -7
- 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/migration/README.md +18 -0
- package/docs/migration/database-changes-december-2025.md +767 -0
- package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
- 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 -3
- 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 +19 -19
- package/src/__tests__/rls-policies.test.ts +210 -74
- package/src/components/AddressField/AddressField.test.tsx +42 -0
- package/src/components/AddressField/AddressField.tsx +71 -60
- package/src/components/AddressField/README.md +7 -6
- package/src/components/Alert/Alert.test.tsx +50 -10
- package/src/components/Alert/Alert.tsx +5 -3
- package/src/components/Avatar/Avatar.test.tsx +95 -43
- package/src/components/Avatar/Avatar.tsx +16 -16
- 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 +42 -22
- 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.test.tsx +4 -1
- package/src/components/Select/Select.tsx +65 -20
- 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.tsx +3 -3
- 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.simple.test.ts +1 -1
- package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
- package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEvent.ts +10 -10
- package/src/hooks/public/usePublicFileDisplay.ts +173 -87
- package/src/hooks/useAppConfig.ts +24 -5
- package/src/hooks/useFileDisplay.ts +298 -36
- 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 +27 -6
- package/src/hooks/useSecureDataAccess.test.ts +87 -42
- package/src/hooks/useSecureDataAccess.ts +95 -48
- package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
- 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 +8 -3
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
- package/src/rbac/utils/contextValidator.ts +288 -0
- package/src/rbac/utils/eventContext.ts +52 -3
- package/src/services/AuthService.ts +37 -8
- package/src/services/EventService.ts +165 -21
- package/src/services/OrganisationService.ts +125 -137
- package/src/services/__tests__/EventService.test.ts +26 -21
- package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
- package/src/services/__tests__/OrganisationService.test.ts +218 -86
- package/src/types/database.generated.ts +166 -201
- package/src/types/file-reference.ts +13 -10
- package/src/types/supabase.ts +2 -2
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
- package/src/utils/app/appNameResolver.test.ts +346 -73
- package/src/utils/context/superAdminOverride.ts +58 -0
- package/src/utils/file-reference/index.ts +65 -37
- package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
- package/src/utils/google-places/googlePlacesUtils.ts +1 -1
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
- package/src/utils/google-places/types.ts +1 -1
- package/src/utils/request-deduplication.ts +4 -4
- package/src/utils/security/secureDataAccess.test.ts +1 -1
- package/src/utils/security/secureDataAccess.ts +7 -4
- package/src/utils/storage/README.md +1 -1
- 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-F2IMUDXZ.js.map +0 -1
- package/dist/chunk-HEHYGYOX.js.map +0 -1
- package/dist/chunk-IM4QE42D.js.map +0 -1
- package/dist/chunk-MX64ZF6I.js.map +0 -1
- package/dist/chunk-SAUPYVLF.js.map +0 -1
- package/dist/chunk-THRPYOFK.js.map +0 -1
- package/dist/chunk-UCQSRW7Z.js.map +0 -1
- package/dist/chunk-VGZZXKBR.js.map +0 -1
- package/dist/chunk-YGPFYGA6.js.map +0 -1
- package/dist/chunk-YHCN776L.js.map +0 -1
- /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
- /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.js.map} +0 -0
- /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
|
@@ -16,6 +16,9 @@ import { Organisation } from '../types/organisation';
|
|
|
16
16
|
import { assertOrganisationId } from '../types/core';
|
|
17
17
|
import { logger } from '../utils/core/logger';
|
|
18
18
|
import { secureStorage } from '../utils/security/secureStorage';
|
|
19
|
+
import { isSuperAdmin, getAppConfigByName } from '../rbac/api';
|
|
20
|
+
import type { UUID } from '../rbac/types';
|
|
21
|
+
import type { AppConfig } from '../rbac/utils/contextValidator';
|
|
19
22
|
|
|
20
23
|
export class EventService extends BaseService implements IEventService {
|
|
21
24
|
private events: Event[] = [];
|
|
@@ -30,6 +33,8 @@ export class EventService extends BaseService implements IEventService {
|
|
|
30
33
|
private appName: string = '';
|
|
31
34
|
private selectedOrganisation: Organisation | null = null;
|
|
32
35
|
private setSelectedEventId: ((eventId: string | null) => void) | null = null;
|
|
36
|
+
private isSuperAdmin: boolean = false; // Track super admin status for conditional validation
|
|
37
|
+
private appConfig: AppConfig | null = null; // Cache app config to avoid repeated lookups
|
|
33
38
|
|
|
34
39
|
// Internal state management
|
|
35
40
|
private isInitializedRef = false;
|
|
@@ -76,6 +81,7 @@ export class EventService extends BaseService implements IEventService {
|
|
|
76
81
|
const newOrgId = selectedOrganisation?.id;
|
|
77
82
|
const previousUserId = this.user?.id || null;
|
|
78
83
|
const newUserId = user?.id || null;
|
|
84
|
+
const previousAppName = this.appName;
|
|
79
85
|
|
|
80
86
|
// If user changed, clear previous user's event selection from storage
|
|
81
87
|
if (previousUserId !== newUserId) {
|
|
@@ -87,6 +93,10 @@ export class EventService extends BaseService implements IEventService {
|
|
|
87
93
|
this.selectedEvent = null;
|
|
88
94
|
this.setSelectedEventId?.(null);
|
|
89
95
|
}
|
|
96
|
+
// Reset initialization when user changes
|
|
97
|
+
this.resetInitialization();
|
|
98
|
+
this.isInitializedRef = false;
|
|
99
|
+
this.isFetchingRef = false;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
this.supabaseClient = supabaseClient;
|
|
@@ -96,8 +106,31 @@ export class EventService extends BaseService implements IEventService {
|
|
|
96
106
|
this.selectedOrganisation = selectedOrganisation;
|
|
97
107
|
this.setSelectedEventId = setSelectedEventId;
|
|
98
108
|
|
|
99
|
-
//
|
|
100
|
-
|
|
109
|
+
// Clear app config cache when app name changes
|
|
110
|
+
if (previousAppName !== appName) {
|
|
111
|
+
this.appConfig = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update super admin status when user changes
|
|
115
|
+
// This allows super admins to select events from any organisation
|
|
116
|
+
if (user?.id) {
|
|
117
|
+
try {
|
|
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
|
+
} catch (error) {
|
|
124
|
+
logger.warn('EventService', 'Failed to check super admin status', { error });
|
|
125
|
+
this.isSuperAdmin = false; // Default to false on error
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
this.isSuperAdmin = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// If organisation changed (from null to value, or different org, or value to null), reset initialization
|
|
132
|
+
// This ensures events are re-fetched when organisation context changes
|
|
133
|
+
// For event-required apps, selectedOrganisation will be null, so we need to reset when it changes from undefined/null to null
|
|
101
134
|
if (previousOrgId !== newOrgId) {
|
|
102
135
|
this.resetInitialization(); // Reset BaseService's isInitialized flag
|
|
103
136
|
this.isInitializedRef = false;
|
|
@@ -138,20 +171,9 @@ export class EventService extends BaseService implements IEventService {
|
|
|
138
171
|
// Event methods
|
|
139
172
|
setSelectedEvent(event: Event | null): void {
|
|
140
173
|
if (event) {
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
logger.error('EventService', 'Event organisation_id does not match selected organisation', {
|
|
145
|
-
eventOrganisationId: event.organisation_id,
|
|
146
|
-
selectedOrganisationId: this.selectedOrganisation.id,
|
|
147
|
-
eventName: event.event_name
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
logger.error('EventService', 'Error during event validation:', error);
|
|
153
|
-
}
|
|
154
|
-
|
|
174
|
+
// No validation needed: For event-required apps, org is derived from event
|
|
175
|
+
// For org-required apps, event is optional and doesn't need validation
|
|
176
|
+
// RLS policies handle security at the database level
|
|
155
177
|
this.selectedEvent = event;
|
|
156
178
|
this.setSelectedEventId?.(event.event_id);
|
|
157
179
|
// Persist asynchronously (don't await to avoid blocking)
|
|
@@ -286,6 +308,12 @@ export class EventService extends BaseService implements IEventService {
|
|
|
286
308
|
async initialize(): Promise<void> {
|
|
287
309
|
// Only call super.initialize() which will call doInitialize() and fetchEvents()
|
|
288
310
|
// 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
|
+
});
|
|
289
317
|
await super.initialize();
|
|
290
318
|
}
|
|
291
319
|
|
|
@@ -294,13 +322,23 @@ export class EventService extends BaseService implements IEventService {
|
|
|
294
322
|
}
|
|
295
323
|
|
|
296
324
|
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
|
+
|
|
297
333
|
// Skip if already initialized
|
|
298
334
|
if (this.isInitializedRef) {
|
|
335
|
+
logger.debug('EventService', 'Skipping initialization - already initialized');
|
|
299
336
|
return;
|
|
300
337
|
}
|
|
301
338
|
|
|
302
339
|
// Skip if already fetching
|
|
303
340
|
if (this.isFetchingRef) {
|
|
341
|
+
logger.debug('EventService', 'Skipping initialization - already fetching');
|
|
304
342
|
return;
|
|
305
343
|
}
|
|
306
344
|
|
|
@@ -313,13 +351,25 @@ export class EventService extends BaseService implements IEventService {
|
|
|
313
351
|
logger.warn('EventService', 'Failed to clean up old storage keys:', error);
|
|
314
352
|
}
|
|
315
353
|
|
|
316
|
-
// Skip if no user
|
|
317
|
-
|
|
354
|
+
// Skip if no user
|
|
355
|
+
// For event-required apps, selectedOrganisation may be null (org derived from event)
|
|
356
|
+
// For org-required apps, selectedOrganisation is required
|
|
357
|
+
if (!this.user) {
|
|
358
|
+
logger.debug('EventService', 'Skipping initialization - missing user');
|
|
318
359
|
return;
|
|
319
360
|
}
|
|
320
361
|
|
|
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
|
+
|
|
321
368
|
// Initial setup - fetch events on initialization
|
|
322
369
|
await this.fetchEvents(false);
|
|
370
|
+
|
|
371
|
+
// Mark as initialized after successful fetch
|
|
372
|
+
this.isInitializedRef = true;
|
|
323
373
|
}
|
|
324
374
|
|
|
325
375
|
protected doCleanup(): void {
|
|
@@ -327,7 +377,15 @@ export class EventService extends BaseService implements IEventService {
|
|
|
327
377
|
}
|
|
328
378
|
|
|
329
379
|
private async fetchEvents(skipLoadPersisted: boolean = false): Promise<void> {
|
|
330
|
-
|
|
380
|
+
// For event-required apps, selectedOrganisation may be null (org derived from event)
|
|
381
|
+
// For org-required apps, selectedOrganisation is required
|
|
382
|
+
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
|
+
});
|
|
331
389
|
// Already false from initialization, just notify
|
|
332
390
|
this.notify();
|
|
333
391
|
return;
|
|
@@ -339,6 +397,7 @@ export class EventService extends BaseService implements IEventService {
|
|
|
339
397
|
|
|
340
398
|
// Prevent multiple simultaneous fetches
|
|
341
399
|
if (this.isFetchingRef) {
|
|
400
|
+
logger.debug('EventService', 'Skipping fetchEvents - already fetching');
|
|
342
401
|
return;
|
|
343
402
|
}
|
|
344
403
|
|
|
@@ -346,12 +405,97 @@ export class EventService extends BaseService implements IEventService {
|
|
|
346
405
|
let isMounted = true;
|
|
347
406
|
|
|
348
407
|
try {
|
|
408
|
+
// Load app config if not already cached (only once per app)
|
|
409
|
+
if (!this.appConfig && this.appName) {
|
|
410
|
+
try {
|
|
411
|
+
this.appConfig = await getAppConfigByName(this.appName);
|
|
412
|
+
} catch (configError) {
|
|
413
|
+
logger.warn('EventService', 'Failed to load app config, defaulting to event-required', {
|
|
414
|
+
error: configError
|
|
415
|
+
});
|
|
416
|
+
// Default to event-required for safety
|
|
417
|
+
this.appConfig = { requires_event: true };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Determine organisationId for RPC call
|
|
422
|
+
// For event-required apps: org is derived from selectedEvent (if available), or null to get all accessible events
|
|
423
|
+
// For org-required apps: org comes from selectedOrganisation
|
|
424
|
+
// Super admins: pass null to see all events
|
|
425
|
+
let organisationIdForRpc: string | null = null;
|
|
426
|
+
|
|
427
|
+
// Check if user is super admin first
|
|
428
|
+
let userIsSuperAdmin = false;
|
|
429
|
+
try {
|
|
430
|
+
userIsSuperAdmin = await isSuperAdmin(this.user.id as UUID);
|
|
431
|
+
if (userIsSuperAdmin) {
|
|
432
|
+
// Super admin: Pass null to see all events across all organisations
|
|
433
|
+
organisationIdForRpc = null;
|
|
434
|
+
logger.debug('EventService', 'Super admin detected - fetching all events', {
|
|
435
|
+
userId: this.user.id
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
// Not super admin: determine org from context based on app type
|
|
439
|
+
if (this.selectedEvent) {
|
|
440
|
+
// If event is already selected, use its organisation
|
|
441
|
+
organisationIdForRpc = this.selectedEvent.organisation_id;
|
|
442
|
+
} else if (this.appConfig?.requires_event === true) {
|
|
443
|
+
// Event-required app with no selected event yet: pass null to get all accessible events
|
|
444
|
+
// The RPC will filter by event app roles, returning all events the user has access to
|
|
445
|
+
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
|
+
} else if (this.selectedOrganisation) {
|
|
451
|
+
// Org-required app: use selected organisation
|
|
452
|
+
organisationIdForRpc = this.selectedOrganisation.id;
|
|
453
|
+
} else {
|
|
454
|
+
// No context available - this shouldn't happen for authenticated users
|
|
455
|
+
logger.warn('EventService', 'No organisation context available for event fetch', {
|
|
456
|
+
hasSelectedEvent: !!this.selectedEvent,
|
|
457
|
+
hasSelectedOrganisation: !!this.selectedOrganisation,
|
|
458
|
+
appRequiresEvent: this.appConfig?.requires_event
|
|
459
|
+
});
|
|
460
|
+
organisationIdForRpc = null; // Will return empty list
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch (superAdminCheckError) {
|
|
464
|
+
// If super admin check fails, fall back to organisation-scoped query
|
|
465
|
+
logger.warn('EventService', 'Failed to check super admin status, using organisation-scoped query', {
|
|
466
|
+
error: superAdminCheckError
|
|
467
|
+
});
|
|
468
|
+
// Fallback: use available context
|
|
469
|
+
if (this.selectedEvent) {
|
|
470
|
+
organisationIdForRpc = this.selectedEvent.organisation_id;
|
|
471
|
+
} else if (this.appConfig?.requires_event === true) {
|
|
472
|
+
// Event-required app: pass null to get all accessible events
|
|
473
|
+
organisationIdForRpc = null;
|
|
474
|
+
} else if (this.selectedOrganisation) {
|
|
475
|
+
organisationIdForRpc = this.selectedOrganisation.id;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
logger.debug('EventService', 'Fetching events via RPC', {
|
|
480
|
+
userId: this.user.id,
|
|
481
|
+
organisationId: organisationIdForRpc,
|
|
482
|
+
appName: this.appName
|
|
483
|
+
});
|
|
484
|
+
|
|
349
485
|
// Call the RPC function following the established pattern
|
|
350
|
-
|
|
486
|
+
// For super admins, pass null for p_organisation_id to see all events
|
|
487
|
+
let { data, error: rpcError } = await this.supabaseClient.rpc('data_user_events_get', {
|
|
351
488
|
p_user_id: this.user.id,
|
|
352
|
-
p_organisation_id:
|
|
489
|
+
p_organisation_id: organisationIdForRpc,
|
|
353
490
|
p_app_name: this.appName
|
|
354
491
|
});
|
|
492
|
+
|
|
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
|
+
});
|
|
355
499
|
|
|
356
500
|
if (rpcError) {
|
|
357
501
|
logger.error('EventService', 'RPC error fetching events:', rpcError);
|
|
@@ -27,6 +27,15 @@ interface OrganisationRoleRpcResponse {
|
|
|
27
27
|
organisation_id: string;
|
|
28
28
|
role: 'org_admin' | 'leader' | 'member' | 'supporter';
|
|
29
29
|
status: 'active' | 'inactive' | 'suspended';
|
|
30
|
+
// Organisation fields from RPC
|
|
31
|
+
name?: string;
|
|
32
|
+
display_name?: string;
|
|
33
|
+
subscription_tier?: string;
|
|
34
|
+
settings?: unknown;
|
|
35
|
+
is_active?: boolean;
|
|
36
|
+
parent_id?: string;
|
|
37
|
+
organisation_created_at?: string;
|
|
38
|
+
organisation_updated_at?: string;
|
|
30
39
|
[key: string]: unknown;
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -71,6 +80,21 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
71
80
|
|
|
72
81
|
// Additional methods for testing
|
|
73
82
|
setSelectedOrganisation(organisation: Organisation | null): void {
|
|
83
|
+
// SECURITY: Validate organisation is in user's accessible organisations (only if orgs are loaded)
|
|
84
|
+
if (organisation && this._organisations.length > 0) {
|
|
85
|
+
const isValidOrg = this._organisations.some(org => org.id === organisation.id);
|
|
86
|
+
if (!isValidOrg) {
|
|
87
|
+
logger.warn('OrganisationService', 'Attempted to set invalid organisation - not in user\'s accessible organisations', {
|
|
88
|
+
organisationId: organisation.id,
|
|
89
|
+
organisationName: organisation.name,
|
|
90
|
+
accessibleOrgIds: this._organisations.map(o => o.id)
|
|
91
|
+
});
|
|
92
|
+
// Don't set invalid organisation - this prevents security issues
|
|
93
|
+
// If organisations haven't loaded yet, validation will happen in loadUserOrganisations()
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
this._selectedOrganisation = organisation;
|
|
75
99
|
if (organisation) {
|
|
76
100
|
localStorage.setItem('pace-core-selected-organisation', JSON.stringify(organisation));
|
|
@@ -343,159 +367,106 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
343
367
|
this.notify();
|
|
344
368
|
|
|
345
369
|
try {
|
|
346
|
-
// Get user's organisation
|
|
347
|
-
//
|
|
348
|
-
|
|
370
|
+
// Get user's organisation roles directly from rbac_organisation_roles table
|
|
371
|
+
// This queries the source table directly instead of using the RPC which filters to match core_organisation_memberships view
|
|
372
|
+
// We still filter to active, non-revoked roles for the org selector
|
|
373
|
+
// The join includes organisation data, so we don't need a separate query that might be filtered by RLS
|
|
374
|
+
let memberships, membershipError, organisations: Organisation[] = [];
|
|
349
375
|
try {
|
|
350
|
-
//
|
|
351
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
352
|
-
const timeoutId = setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 10000);
|
|
353
|
-
abortSignal.addEventListener('abort', () => {
|
|
354
|
-
clearTimeout(timeoutId);
|
|
355
|
-
reject(new Error('Request aborted'));
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
const rpcPromise = this.supabaseClient.rpc('data_user_organisation_roles_get', {
|
|
360
|
-
p_user_id: this.user.id,
|
|
361
|
-
p_organisation_id: null
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// Check if request was aborted before making the call
|
|
376
|
+
// Check if request was aborted before making query
|
|
365
377
|
if (abortSignal.aborted) {
|
|
366
378
|
throw new Error('Request aborted');
|
|
367
379
|
}
|
|
380
|
+
|
|
381
|
+
const { data: rolesData, error: rolesError } = await this.supabaseClient
|
|
382
|
+
.from('rbac_organisation_roles')
|
|
383
|
+
.select(`
|
|
384
|
+
id,
|
|
385
|
+
user_id,
|
|
386
|
+
organisation_id,
|
|
387
|
+
role,
|
|
388
|
+
status,
|
|
389
|
+
granted_at,
|
|
390
|
+
granted_by,
|
|
391
|
+
revoked_at,
|
|
392
|
+
revoked_by,
|
|
393
|
+
notes,
|
|
394
|
+
created_at,
|
|
395
|
+
updated_at,
|
|
396
|
+
core_organisations!inner(
|
|
397
|
+
id,
|
|
398
|
+
name,
|
|
399
|
+
display_name,
|
|
400
|
+
subscription_tier,
|
|
401
|
+
settings,
|
|
402
|
+
is_active,
|
|
403
|
+
parent_id,
|
|
404
|
+
created_at,
|
|
405
|
+
updated_at
|
|
406
|
+
)
|
|
407
|
+
`)
|
|
408
|
+
.eq('user_id', this.user.id)
|
|
409
|
+
.eq('status', 'active')
|
|
410
|
+
.is('revoked_at', null);
|
|
368
411
|
|
|
369
|
-
|
|
412
|
+
if (rolesError) {
|
|
413
|
+
logger.error("OrganisationService", "Error loading organisation roles:", rolesError);
|
|
414
|
+
throw rolesError;
|
|
415
|
+
}
|
|
370
416
|
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
memberships = result.data?.filter((role) =>
|
|
375
|
-
['org_admin', 'leader', 'member', 'supporter'].includes(role.role)
|
|
376
|
-
).map((m) => ({
|
|
417
|
+
// Map to branded types and extract organisation data from the join
|
|
418
|
+
// The join already includes organisation data, so we don't need a separate query
|
|
419
|
+
memberships = rolesData?.map((m) => ({
|
|
377
420
|
...m,
|
|
378
421
|
user_id: assertUserId(m.user_id),
|
|
379
422
|
organisation_id: assertOrganisationId(m.organisation_id),
|
|
380
423
|
})) || [];
|
|
381
|
-
membershipError = result.error;
|
|
382
|
-
} catch (queryError) {
|
|
383
|
-
membershipError = queryError instanceof Error ? queryError : new Error(String(queryError));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (membershipError) {
|
|
387
|
-
logger.error("OrganisationService", "Error loading memberships:", membershipError);
|
|
388
424
|
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
.
|
|
399
|
-
.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
revoked_by,
|
|
409
|
-
notes,
|
|
410
|
-
created_at,
|
|
411
|
-
updated_at,
|
|
412
|
-
organisations!inner(
|
|
413
|
-
id,
|
|
414
|
-
name,
|
|
415
|
-
display_name,
|
|
416
|
-
subscription_tier,
|
|
417
|
-
settings,
|
|
418
|
-
is_active,
|
|
419
|
-
parent_id,
|
|
420
|
-
created_at,
|
|
421
|
-
updated_at
|
|
422
|
-
)
|
|
423
|
-
`)
|
|
424
|
-
.eq('user_id', this.user.id)
|
|
425
|
-
.eq('status', 'active')
|
|
426
|
-
.is('revoked_at', null)
|
|
427
|
-
.in('role', ['org_admin', 'leader', 'member', 'supporter']);
|
|
428
|
-
|
|
429
|
-
if (fallbackError) {
|
|
430
|
-
logger.error("OrganisationService", "Fallback query also failed:", fallbackError);
|
|
431
|
-
throw membershipError; // Throw original error
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Map to branded types
|
|
435
|
-
memberships = fallbackData?.map((m) => ({
|
|
436
|
-
...m,
|
|
437
|
-
user_id: assertUserId(m.user_id),
|
|
438
|
-
organisation_id: assertOrganisationId(m.organisation_id),
|
|
439
|
-
})) || [];
|
|
440
|
-
membershipError = null;
|
|
441
|
-
} catch (fallbackErr) {
|
|
442
|
-
logger.error("OrganisationService", "Fallback query failed:", fallbackErr);
|
|
443
|
-
throw membershipError; // Throw original error
|
|
425
|
+
// Extract unique organisations from the join results
|
|
426
|
+
// Use a Map to deduplicate by organisation ID
|
|
427
|
+
// Supabase returns joined data nested under the relation name
|
|
428
|
+
const organisationsMap = new Map<string, Organisation>();
|
|
429
|
+
rolesData?.forEach((role: any) => {
|
|
430
|
+
// The join returns organisation data nested under 'core_organisations' key
|
|
431
|
+
const orgData = role.core_organisations;
|
|
432
|
+
if (orgData && role.organisation_id && !organisationsMap.has(role.organisation_id)) {
|
|
433
|
+
organisationsMap.set(role.organisation_id, {
|
|
434
|
+
id: orgData.id,
|
|
435
|
+
name: orgData.name,
|
|
436
|
+
display_name: orgData.display_name,
|
|
437
|
+
subscription_tier: orgData.subscription_tier,
|
|
438
|
+
settings: orgData.settings,
|
|
439
|
+
is_active: orgData.is_active,
|
|
440
|
+
parent_id: orgData.parent_id,
|
|
441
|
+
created_at: orgData.created_at,
|
|
442
|
+
updated_at: orgData.updated_at,
|
|
443
|
+
} as Organisation);
|
|
444
444
|
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
organisations = Array.from(organisationsMap.values());
|
|
448
|
+
|
|
449
|
+
// Extract organisations from join results
|
|
450
|
+
} catch (queryError) {
|
|
451
|
+
// Extract error message properly from Supabase error objects
|
|
452
|
+
if (queryError instanceof Error) {
|
|
453
|
+
membershipError = queryError;
|
|
454
|
+
} else if (queryError && typeof queryError === 'object' && 'message' in queryError) {
|
|
455
|
+
membershipError = new Error(String((queryError as any).message));
|
|
445
456
|
} else {
|
|
446
|
-
|
|
457
|
+
membershipError = new Error(String(queryError));
|
|
447
458
|
}
|
|
459
|
+
logger.error("OrganisationService", "Error loading organisation roles:", membershipError);
|
|
460
|
+
throw membershipError;
|
|
448
461
|
}
|
|
449
462
|
|
|
450
463
|
if (!memberships || memberships.length === 0) {
|
|
451
464
|
throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
|
|
452
465
|
}
|
|
453
466
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
.map((m) => m.organisation_id)
|
|
457
|
-
.filter((id: string) => {
|
|
458
|
-
// Better validation to prevent empty string UUID errors
|
|
459
|
-
if (!id || typeof id !== 'string') {
|
|
460
|
-
logger.warn("OrganisationService", "Invalid organisation ID (not string):", id);
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
const trimmedId = id.trim();
|
|
464
|
-
if (trimmedId === '') {
|
|
465
|
-
logger.warn("OrganisationService", "Empty organisation ID found");
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
// Validate UUID format
|
|
469
|
-
const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId);
|
|
470
|
-
if (!isValidUuid) {
|
|
471
|
-
logger.warn("OrganisationService", "Invalid UUID format:", trimmedId);
|
|
472
|
-
}
|
|
473
|
-
return isValidUuid;
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
if (organisationIds.length === 0) {
|
|
477
|
-
logger.warn("OrganisationService", "No valid organisation IDs found in memberships:", memberships);
|
|
478
|
-
throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Check if request was aborted before making organisations query
|
|
482
|
-
if (abortSignal.aborted) {
|
|
483
|
-
throw new Error('Request aborted');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const { data: allOrganisations, error: orgError } = await this.supabaseClient
|
|
487
|
-
.from('organisations')
|
|
488
|
-
.select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at');
|
|
489
|
-
|
|
490
|
-
if (orgError) {
|
|
491
|
-
logger.error("OrganisationService", "Error loading organisations:", orgError);
|
|
492
|
-
throw orgError;
|
|
467
|
+
if (!organisations || organisations.length === 0) {
|
|
468
|
+
throw new Error('No organisations found in role data') as OrganisationSecurityError;
|
|
493
469
|
}
|
|
494
|
-
|
|
495
|
-
// Filter manually on the client side
|
|
496
|
-
const organisations = allOrganisations?.filter(org =>
|
|
497
|
-
organisationIds.includes(org.id)
|
|
498
|
-
) || [];
|
|
499
470
|
|
|
500
471
|
// Create a map of organisation_id to role from the memberships data
|
|
501
472
|
const roleMap = new Map<string, string>();
|
|
@@ -507,6 +478,8 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
507
478
|
const orgs = organisations as Organisation[];
|
|
508
479
|
const activeOrgs = orgs.filter(org => org.is_active);
|
|
509
480
|
|
|
481
|
+
// Filter to active organisations only
|
|
482
|
+
|
|
510
483
|
if (activeOrgs.length === 0) {
|
|
511
484
|
throw new Error('User has no access to active organisations') as OrganisationSecurityError;
|
|
512
485
|
}
|
|
@@ -534,16 +507,13 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
534
507
|
initialOrg = validPersistedOrg;
|
|
535
508
|
selectionMethod = 'persisted';
|
|
536
509
|
} else {
|
|
537
|
-
logger.warn("OrganisationService", "Persisted organisation not found in active orgs, clearing cache");
|
|
538
510
|
localStorage.removeItem('pace-core-selected-organisation');
|
|
539
511
|
}
|
|
540
512
|
} else {
|
|
541
|
-
logger.warn("OrganisationService", "Invalid persisted organisation ID, clearing cache");
|
|
542
513
|
localStorage.removeItem('pace-core-selected-organisation');
|
|
543
514
|
}
|
|
544
515
|
}
|
|
545
516
|
} catch (storageError) {
|
|
546
|
-
logger.warn("OrganisationService", "Failed to restore persisted organisation:", storageError);
|
|
547
517
|
// Clear potentially corrupted cache
|
|
548
518
|
localStorage.removeItem('pace-core-selected-organisation');
|
|
549
519
|
}
|
|
@@ -570,6 +540,16 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
570
540
|
throw new Error('No valid organisation found for user') as OrganisationSecurityError;
|
|
571
541
|
}
|
|
572
542
|
|
|
543
|
+
// SECURITY: Validate current selected organisation is still valid (in case it was set before orgs loaded)
|
|
544
|
+
const currentSelectedOrg = this._selectedOrganisation;
|
|
545
|
+
if (currentSelectedOrg && !activeOrgs.some(org => org.id === currentSelectedOrg.id)) {
|
|
546
|
+
logger.warn('OrganisationService', 'Current selected organisation is no longer valid, resetting', {
|
|
547
|
+
invalidOrgId: currentSelectedOrg.id,
|
|
548
|
+
validOrgIds: activeOrgs.map(o => o.id)
|
|
549
|
+
});
|
|
550
|
+
this._selectedOrganisation = null;
|
|
551
|
+
}
|
|
552
|
+
|
|
573
553
|
this._selectedOrganisation = initialOrg;
|
|
574
554
|
|
|
575
555
|
// Persist selection
|
|
@@ -583,14 +563,22 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
583
563
|
this.hasFailedRef = false;
|
|
584
564
|
|
|
585
565
|
} catch (err) {
|
|
586
|
-
|
|
587
|
-
|
|
566
|
+
const error = err as Error;
|
|
567
|
+
// "User has no access to active organisations" is a valid state for users without orgs (e.g., profile pages)
|
|
568
|
+
// Only log actual errors, not expected states
|
|
569
|
+
if (error.message !== 'User has no access to active organisations') {
|
|
570
|
+
logger.error("OrganisationService", "Failed to load organisations:", err);
|
|
571
|
+
}
|
|
572
|
+
this._error = error;
|
|
588
573
|
// Increment retry count on error
|
|
589
574
|
this.retryCount = this.retryCount + 1;
|
|
590
575
|
// Set failed flag to prevent further attempts
|
|
591
576
|
this.hasFailedRef = true;
|
|
592
577
|
// Clear all cached data on error to prevent corruption
|
|
593
578
|
this.clearAllCachedData();
|
|
579
|
+
// Mark context as ready even on error - this allows the app to proceed
|
|
580
|
+
// The app can check hasValidOrganisationContext() to determine if org context is available
|
|
581
|
+
this._isContextReady = true;
|
|
594
582
|
} finally {
|
|
595
583
|
// Always cleanup refs and abort controller
|
|
596
584
|
this.isLoadingRef = false;
|