@jmruthers/pace-core 0.5.67 → 0.5.69
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/README.md +26 -0
- package/dist/{DataTable-MFUXNGPR.js → DataTable-MPBSXUC6.js} +5 -6
- package/dist/{PublicLoadingSpinner-DdKXTkCZ.d.ts → PublicLoadingSpinner-BOdyU3u-.d.ts} +1 -1
- package/dist/{UnifiedAuthProvider-CQNiemcB.d.ts → UnifiedAuthProvider-D02AMXgO.d.ts} +3 -3
- package/dist/{chunk-CKNY7HYS.js → chunk-2ARQW6VX.js} +3 -3
- package/dist/{chunk-T2MQY57J.js → chunk-6JILXFEA.js} +335 -5
- package/dist/chunk-6JILXFEA.js.map +1 -0
- package/dist/{chunk-D7ARGIA3.js → chunk-6RBH67W7.js} +23 -6
- package/dist/chunk-6RBH67W7.js.map +1 -0
- package/dist/{chunk-C7GUF747.js → chunk-FJTAWPAQ.js} +3 -5
- package/dist/{chunk-C7GUF747.js.map → chunk-FJTAWPAQ.js.map} +1 -1
- package/dist/{chunk-4HQ5BOVZ.js → chunk-NO5QHMDX.js} +7 -6
- package/dist/chunk-NO5QHMDX.js.map +1 -0
- package/dist/{chunk-ZPK5656W.js → chunk-O3NWNXDY.js} +4 -5
- package/dist/chunk-O3NWNXDY.js.map +1 -0
- package/dist/{chunk-BTCA3ENN.js → chunk-Q2UP3ZWQ.js} +4 -4
- package/dist/{chunk-QVEOQVD4.js → chunk-RVYGJPOD.js} +173 -20
- package/dist/chunk-RVYGJPOD.js.map +1 -0
- package/dist/{chunk-FVDOEGGG.js → chunk-UCMHBF7Y.js} +3 -5
- package/dist/{chunk-FVDOEGGG.js.map → chunk-UCMHBF7Y.js.map} +1 -1
- package/dist/{chunk-T6HVDA24.js → chunk-V3QO3LL7.js} +5 -7
- package/dist/chunk-V3QO3LL7.js.map +1 -0
- package/dist/{chunk-ZB6AEA7I.js → chunk-ZXJGZLLO.js} +17 -17
- package/dist/{chunk-ZB6AEA7I.js.map → chunk-ZXJGZLLO.js.map} +1 -1
- package/dist/components.d.ts +2 -2
- package/dist/components.js +8 -9
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +9 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +16 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +5 -7
- package/dist/rbac/index.js +5 -6
- package/dist/{usePublicRouteParams-CdoFxnJK.d.ts → usePublicRouteParams-Ua1Vz-HG.d.ts} +35 -1
- package/dist/utils.d.ts +4 -1
- package/dist/utils.js +3 -3
- package/docs/DOCUMENTATION_CHECKLIST.md +281 -0
- package/docs/README.md +22 -10
- package/docs/api/README.md +26 -0
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +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 +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +2 -2
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +4 -4
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.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 +39 -14
- package/docs/api-reference/providers.md +16 -7
- package/docs/architecture/services.md +374 -0
- package/docs/best-practices/README.md +1 -1
- package/docs/best-practices/testing.md +1 -1
- package/docs/breaking-changes.md +182 -0
- package/docs/common-patterns.md +445 -0
- package/docs/core-concepts/authentication.md +26 -11
- package/docs/core-concepts/events.md +2 -0
- package/docs/core-concepts/organisations.md +2 -0
- package/docs/core-concepts/permissions.md +2 -0
- package/docs/{INDEX.md → documentation-index.md} +26 -38
- package/docs/faq.md +286 -0
- package/docs/{FILE_REFERENCE_SYSTEM.md → file-reference-system.md} +1 -1
- package/docs/getting-started/installation-guide.md +284 -0
- package/docs/getting-started/quick-start.md +8 -1
- package/docs/implementation-guides/app-layout.md +3 -1
- package/docs/implementation-guides/data-tables.md +2 -0
- package/docs/implementation-guides/dynamic-colors.md +47 -2
- package/docs/implementation-guides/event-theming-summary.md +220 -0
- package/docs/implementation-guides/forms.md +9 -7
- package/docs/implementation-guides/navigation.md +2 -0
- package/docs/migration/service-architecture.md +351 -0
- package/docs/migration-guides/unified-auth-provider-mandatory-timeouts.md +226 -0
- package/docs/rbac/README-rbac-rls-integration.md +2 -2
- package/docs/rbac/README.md +1 -1
- package/docs/rbac/examples/rbac-rls-integration-example.md +3 -3
- package/docs/rbac/quick-start.md +2 -0
- package/docs/rbac/rbac-rls-integration.md +2 -2
- package/docs/security/README.md +5 -1
- package/docs/style-guide.md +136 -1
- package/docs/testing/README.md +1 -1
- package/docs/troubleshooting/authentication-issues.md +334 -0
- package/docs/troubleshooting/common-issues.md +2 -0
- package/docs/troubleshooting/styling-issues.md +199 -144
- package/docs/usage.md +23 -2
- package/package.json +1 -1
- package/src/__tests__/{TESTING_GUIDELINES.md → TEST_GUIDE_CURSOR.md} +20 -0
- package/src/__tests__/TEST_GUIDE_HUMAN.md +103 -0
- package/src/__tests__/fixtures/test-data.ts +90 -0
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +260 -0
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +224 -0
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +273 -0
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +98 -0
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +436 -0
- package/src/__tests__/helpers/__tests__/timer-utils.test.ts +371 -0
- package/src/__tests__/helpers/component-test-utils.tsx +14 -4
- package/src/__tests__/helpers/optimized-test-setup.ts +68 -0
- package/src/__tests__/helpers/test-providers.tsx +329 -0
- package/src/__tests__/helpers/test-utils.tsx +91 -45
- package/src/__tests__/helpers/timer-utils.ts +71 -0
- package/src/__tests__/hooks/usePermissions.test.ts +1 -5
- package/src/__tests__/integration/UserProfile.test.tsx +1 -5
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +42 -12
- package/src/__tests__/setup.ts +34 -28
- package/src/components/Alert/Alert.test.tsx +1 -5
- package/src/components/Avatar/Avatar.test.tsx +1 -5
- package/src/components/Button/Button.test.tsx +4 -20
- package/src/components/Card/Card.test.tsx +1 -5
- package/src/components/Checkbox/Checkbox.test.tsx +1 -5
- package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +1 -5
- package/src/components/DataTable/__tests__/DataTable.test.tsx +45 -49
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +1 -5
- package/src/components/DataTable/__tests__/styles.test.ts +382 -0
- package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +409 -0
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +634 -0
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +519 -0
- package/src/components/DataTable/core/__tests__/StateManager.test.ts +714 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +592 -0
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +354 -0
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +539 -0
- package/src/components/Dialog/examples/__tests__/SmartDialogExample.unit.test.tsx +1 -5
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +1 -8
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +34 -38
- package/src/components/Footer/Footer.test.tsx +1 -5
- package/src/components/Form/Form.test.tsx +22 -35
- package/src/components/Header/Header.test.tsx +1 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +1 -5
- package/src/components/Input/Input.test.tsx +2 -10
- package/src/components/LoginForm/LoginForm.test.tsx +1 -5
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +24 -24
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +1 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +6 -16
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +1 -5
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +1 -5
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +1 -7
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +1 -9
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +1 -9
- package/src/components/PublicLayout/PublicErrorBoundary.tsx +4 -5
- package/src/components/PublicLayout/PublicPageHeader.tsx +13 -9
- package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +666 -0
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +457 -0
- package/src/components/PublicLayout/__tests__/PublicLoadingSpinner.test.tsx +393 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +351 -0
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +374 -0
- package/src/components/PublicLayout/__tests__/PublicPageLayout.test.tsx +388 -0
- package/src/components/Select/Select.bug-test.tsx +69 -0
- package/src/components/Select/Select.refactored.tsx +497 -0
- package/src/components/Select/Select.test.tsx +42 -49
- package/src/components/Select/Select.tsx +5 -2
- package/src/components/Select/hooks.ts +254 -0
- package/src/components/Switch/Switch.test.tsx +1 -5
- package/src/components/Table/__tests__/Table.test.tsx +775 -0
- package/src/components/Toast/Toast.test.tsx +15 -8
- package/src/components/Tooltip/Tooltip.test.tsx +1 -5
- package/src/components/UserMenu/UserMenu.test.tsx +3 -15
- package/src/components/__tests__/FileDisplay.test.tsx +575 -0
- package/src/components/__tests__/FileUpload.test.tsx +446 -0
- package/src/components/__tests__/SuperAdminGuard.test.tsx +422 -354
- package/src/hooks/__tests__/ServiceHooks.test.tsx +613 -0
- package/src/hooks/__tests__/hooks.integration.test.tsx +1 -10
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +10 -14
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +307 -0
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +1 -6
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +1 -5
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +6 -9
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +321 -0
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +583 -0
- package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +640 -0
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +435 -0
- package/src/hooks/__tests__/useRBAC.unit.test.ts +10 -10
- package/src/hooks/__tests__/useStorage.unit.test.ts +751 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/public/usePublicEvent.ts +181 -13
- package/src/hooks/public/usePublicRouteParams.ts +13 -3
- package/src/hooks/services/useAuth.ts +50 -0
- package/src/hooks/services/useAuthService.ts +30 -0
- package/src/hooks/services/useCurrentEvent.ts +36 -0
- package/src/hooks/services/useCurrentOrganisation.ts +52 -0
- package/src/hooks/services/useEventService.ts +30 -0
- package/src/hooks/services/useInactivityService.ts +30 -0
- package/src/hooks/services/useOrganisationService.ts +30 -0
- package/src/hooks/services/usePermissions.ts +70 -0
- package/src/hooks/services/useRBACService.ts +30 -0
- package/src/hooks/useCounter.test.ts +1 -5
- package/src/hooks/useEventTheme.ts +86 -0
- package/src/hooks/useOrganisationPermissions.test.ts +2 -5
- package/src/hooks/useOrganisationSecurity.test.ts +1 -5
- package/src/hooks/usePermissionCache.test.ts +1 -5
- package/src/hooks/usePermissionCheck.ts +150 -0
- package/src/hooks/useSecureDataAccess.test.ts +1 -5
- package/src/index.ts +1 -0
- package/src/providers/OrganisationProvider.test.tsx +1 -5
- package/src/providers/OrganisationProvider.tsx +56 -4
- package/src/providers/UnifiedAuthProvider.test.simple.tsx +42 -6
- package/src/providers/UnifiedAuthProvider.test.tsx +1 -5
- package/src/providers/UnifiedAuthProvider.tsx +4 -4
- package/src/providers/__tests__/AuthProvider.test.tsx +105 -439
- package/src/providers/__tests__/AuthProvider.test.tsx.backup +771 -0
- package/src/providers/__tests__/EventProvider.test.tsx +211 -110
- package/src/providers/__tests__/EventProvider.test.tsx.backup +824 -0
- package/src/providers/__tests__/InactivityProvider.test.tsx +1 -5
- package/src/providers/__tests__/OrganisationProvider.test.tsx +97 -261
- package/src/providers/__tests__/OrganisationProvider.test.tsx.backup +820 -0
- package/src/providers/__tests__/ServiceProviders.test.tsx +477 -0
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +72 -504
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup +911 -0
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup2 +166 -0
- package/src/providers/services/AuthServiceProvider.tsx +65 -0
- package/src/providers/services/EventServiceProvider.tsx +83 -0
- package/src/providers/services/InactivityServiceProvider.tsx +83 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +77 -0
- package/src/providers/services/RBACServiceProvider.tsx +79 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +368 -0
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +210 -0
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +269 -0
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +892 -0
- package/src/rbac/__tests__/engine.comprehensive.test.ts +954 -0
- package/src/rbac/__tests__/integration.authflow.test.tsx +1 -5
- package/src/rbac/__tests__/integration.navigation.test.tsx +1 -4
- package/src/rbac/__tests__/rbac-core.test.tsx +2 -7
- package/src/rbac/__tests__/rbac-functions.test.ts +1 -9
- package/src/rbac/__tests__/rbac-integration.test.ts +1 -9
- package/src/rbac/api.test.ts +1 -9
- package/src/rbac/cache.test.ts +10 -8
- package/src/rbac/cli/__tests__/policy-manager.test.ts +339 -0
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -5
- package/src/rbac/components/NavigationProvider.test.tsx +1 -5
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -5
- package/src/rbac/components/SecureDataProvider.test.tsx +1 -5
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +25 -29
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +27 -30
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +23 -27
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +18 -22
- package/src/rbac/config.test.ts +1 -5
- package/src/rbac/hooks/useCan.test.ts +262 -9
- package/src/rbac/hooks/usePermissions.test.ts +246 -6
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.test.ts +472 -198
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +1 -9
- package/src/services/AuthService.ts +416 -0
- package/src/services/EventService.ts +366 -0
- package/src/services/InactivityService.ts +388 -0
- package/src/services/OrganisationService.ts +592 -0
- package/src/services/RBACService.ts +522 -0
- package/src/services/__tests__/AuthService.test.ts +356 -0
- package/src/services/__tests__/BaseService.test.ts +314 -0
- package/src/services/__tests__/EventService.test.ts +489 -0
- package/src/services/__tests__/InactivityService.test.ts +403 -0
- package/src/services/__tests__/OrganisationService.test.ts +660 -0
- package/src/services/__tests__/RBACService.test.ts +492 -0
- package/src/services/base/BaseService.ts +87 -0
- package/src/services/interfaces/IAuthService.ts +39 -0
- package/src/services/interfaces/IEventService.ts +30 -0
- package/src/services/interfaces/IInactivityService.ts +31 -0
- package/src/services/interfaces/IOrganisationService.ts +41 -0
- package/src/services/interfaces/IRBACService.ts +62 -0
- package/src/theming/__tests__/runtime.test.ts +540 -0
- package/src/types/__tests__/file-reference.test.ts +447 -0
- package/src/types/__tests__/organisation.test.ts +1133 -0
- package/src/types/__tests__/theme.test.ts +830 -0
- package/src/types/__tests__/type-validation.test.ts +527 -0
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +1 -5
- package/src/utils/__tests__/debugLogger.test.ts +417 -0
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +1 -6
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +1 -5
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +35 -35
- package/src/utils/__tests__/organisationContext.unit.test.ts +1 -5
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +5 -11
- package/src/utils/__tests__/secureErrors.unit.test.ts +1 -6
- package/src/utils/__tests__/secureStorage.unit.test.ts +1 -5
- package/src/utils/__tests__/securityMonitor.unit.test.ts +1 -5
- package/src/utils/__tests__/sessionTracking.unit.test.ts +1 -5
- package/src/utils/appIdResolver.test.ts +6 -10
- package/src/utils/appNameResolver.simple.test.ts +142 -0
- package/src/utils/appNameResolver.test.ts +31 -458
- package/src/utils/appNameResolver.test.ts.backup +494 -0
- package/src/utils/debugLogger.ts +26 -5
- package/src/utils/formatDate.test.ts +1 -5
- package/src/utils/organisationContext.test.ts +1 -5
- package/src/utils/performanceBudgets.ts +3 -4
- package/src/utils/secureDataAccess.test.ts +1 -5
- package/src/utils/storage/__tests__/helpers.unit.test.ts +1 -5
- package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +1 -5
- package/dist/chunk-4HQ5BOVZ.js.map +0 -1
- package/dist/chunk-D7ARGIA3.js.map +0 -1
- package/dist/chunk-QVEOQVD4.js.map +0 -1
- package/dist/chunk-T2MQY57J.js.map +0 -1
- package/dist/chunk-T6HVDA24.js.map +0 -1
- package/dist/chunk-VTJ5HCZB.js +0 -315
- package/dist/chunk-VTJ5HCZB.js.map +0 -1
- package/dist/chunk-ZPK5656W.js.map +0 -1
- package/docs/getting-started/installation.md +0 -269
- package/src/__tests__/REBUILD_PLAN.md +0 -223
- /package/dist/{DataTable-MFUXNGPR.js.map → DataTable-MPBSXUC6.js.map} +0 -0
- /package/dist/{chunk-CKNY7HYS.js.map → chunk-2ARQW6VX.js.map} +0 -0
- /package/dist/{chunk-BTCA3ENN.js.map → chunk-Q2UP3ZWQ.js.map} +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Organisation Service
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Services
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Organisation service implementation.
|
|
8
|
+
* Handles organisation management and selection with security-first approach.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { type SupabaseClient, type User, type Session } from '@supabase/supabase-js';
|
|
12
|
+
import { BaseService } from './base/BaseService';
|
|
13
|
+
import { IOrganisationService } from './interfaces/IOrganisationService';
|
|
14
|
+
import type {
|
|
15
|
+
Organisation,
|
|
16
|
+
OrganisationMembership,
|
|
17
|
+
OrganisationSecurityError,
|
|
18
|
+
OrganisationHierarchy
|
|
19
|
+
} from '../types/organisation';
|
|
20
|
+
import { setOrganisationContext } from '../utils/organisationContext';
|
|
21
|
+
import { DebugLogger } from '../utils/debugLogger';
|
|
22
|
+
|
|
23
|
+
export class OrganisationService extends BaseService implements IOrganisationService {
|
|
24
|
+
private _selectedOrganisation: Organisation | null = null;
|
|
25
|
+
private _organisations: Organisation[] = [];
|
|
26
|
+
private _userMemberships: OrganisationMembership[] = [];
|
|
27
|
+
private _roleMapState: Map<string, string> = new Map();
|
|
28
|
+
private _isLoading = true;
|
|
29
|
+
private _error: Error | null = null;
|
|
30
|
+
private _isContextReady = false;
|
|
31
|
+
private retryCount = 0;
|
|
32
|
+
|
|
33
|
+
// Dependencies
|
|
34
|
+
private supabaseClient: SupabaseClient | null = null;
|
|
35
|
+
private user: User | null = null;
|
|
36
|
+
private session: Session | null = null;
|
|
37
|
+
|
|
38
|
+
// Internal state management
|
|
39
|
+
private isLoadingRef = false;
|
|
40
|
+
private lastLoadTimeRef = 0;
|
|
41
|
+
private hasFailedRef = false;
|
|
42
|
+
private abortControllerRef: AbortController | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(supabaseClient: SupabaseClient, user: User | null, session: Session | null) {
|
|
45
|
+
super();
|
|
46
|
+
this.supabaseClient = supabaseClient;
|
|
47
|
+
this.user = user;
|
|
48
|
+
this.session = session;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Interface implementation
|
|
52
|
+
getSelectedOrganisation(): Organisation | null { return this._selectedOrganisation; }
|
|
53
|
+
getOrganisations(): Organisation[] { return this._organisations; }
|
|
54
|
+
getUserMemberships(): OrganisationMembership[] { return this._userMemberships; }
|
|
55
|
+
isLoading(): boolean { return this._isLoading; }
|
|
56
|
+
getError(): Error | null { return this._error; }
|
|
57
|
+
hasValidOrganisationContext(): boolean { return !!(this._selectedOrganisation && !this._isLoading && !this._error && this._isContextReady); }
|
|
58
|
+
isContextReady(): boolean { return this._isContextReady; }
|
|
59
|
+
|
|
60
|
+
// Additional methods for testing
|
|
61
|
+
setSelectedOrganisation(organisation: Organisation | null): void {
|
|
62
|
+
this._selectedOrganisation = organisation;
|
|
63
|
+
if (organisation) {
|
|
64
|
+
localStorage.setItem('pace-core-selected-organisation', JSON.stringify(organisation));
|
|
65
|
+
this.setDatabaseOrganisationContext(organisation);
|
|
66
|
+
} else {
|
|
67
|
+
localStorage.removeItem('pace-core-selected-organisation');
|
|
68
|
+
this._isContextReady = false;
|
|
69
|
+
}
|
|
70
|
+
this.notify();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// For testing: expose dependencies
|
|
74
|
+
getDependencies(): { user: User | null; session: Session | null; supabaseClient: SupabaseClient | null } {
|
|
75
|
+
return {
|
|
76
|
+
user: this.user,
|
|
77
|
+
session: this.session,
|
|
78
|
+
supabaseClient: this.supabaseClient
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For testing: manually set state
|
|
83
|
+
setTestState(
|
|
84
|
+
organisations: Organisation[],
|
|
85
|
+
memberships: OrganisationMembership[],
|
|
86
|
+
roleMap: Map<string, string>,
|
|
87
|
+
selectedOrg: Organisation | null = null
|
|
88
|
+
): void {
|
|
89
|
+
this._organisations = organisations;
|
|
90
|
+
this._userMemberships = memberships;
|
|
91
|
+
this._roleMapState = roleMap;
|
|
92
|
+
if (selectedOrg) {
|
|
93
|
+
this._selectedOrganisation = selectedOrg;
|
|
94
|
+
} else if (organisations.length > 0) {
|
|
95
|
+
this._selectedOrganisation = organisations[0];
|
|
96
|
+
}
|
|
97
|
+
this._isLoading = false;
|
|
98
|
+
this._error = null;
|
|
99
|
+
this.notify();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Update dependencies
|
|
103
|
+
updateDependencies(user: User | null, session: Session | null): void {
|
|
104
|
+
this.user = user;
|
|
105
|
+
this.session = session;
|
|
106
|
+
this.notify();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Organisation methods
|
|
110
|
+
async switchOrganisation(orgId: string): Promise<void> {
|
|
111
|
+
DebugLogger.log("OrganisationService", "Switching to organisation:", orgId);
|
|
112
|
+
|
|
113
|
+
// Validate access
|
|
114
|
+
if (!this.validateOrganisationAccess(orgId)) {
|
|
115
|
+
throw new Error(`User does not have access to organisation ${orgId}`) as OrganisationSecurityError;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const targetOrg = this._organisations.find(org => org.id === orgId);
|
|
119
|
+
if (!targetOrg) {
|
|
120
|
+
throw new Error(`Organisation ${orgId} not found in user's organisations`) as OrganisationSecurityError;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this._selectedOrganisation = targetOrg;
|
|
124
|
+
|
|
125
|
+
// Persist selection
|
|
126
|
+
localStorage.setItem('pace-core-selected-organisation', JSON.stringify(targetOrg));
|
|
127
|
+
|
|
128
|
+
// Set database organisation context
|
|
129
|
+
await this.setDatabaseOrganisationContext(targetOrg);
|
|
130
|
+
|
|
131
|
+
DebugLogger.log("OrganisationService", "Switched to organisation:", targetOrg.display_name);
|
|
132
|
+
this.notify();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getUserRole(orgId?: string): string {
|
|
136
|
+
const targetOrgId = orgId || this._selectedOrganisation?.id;
|
|
137
|
+
if (!targetOrgId) return 'no_access';
|
|
138
|
+
|
|
139
|
+
// Use roleMapState to get the role for this organisation
|
|
140
|
+
return this._roleMapState.get(targetOrgId) || 'no_access';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
validateOrganisationAccess(orgId: string): boolean {
|
|
144
|
+
return this._userMemberships.some((m: any) =>
|
|
145
|
+
m.organisation_id === orgId &&
|
|
146
|
+
m.status === 'active' &&
|
|
147
|
+
m.revoked_at === null
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async refreshOrganisations(): Promise<void> {
|
|
152
|
+
if (!this.user || !this.session || !this.supabaseClient) return;
|
|
153
|
+
|
|
154
|
+
// Force reload by triggering the effect
|
|
155
|
+
this._isLoading = true;
|
|
156
|
+
this.notify();
|
|
157
|
+
await this.loadUserOrganisations();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ensureOrganisationContext(): Organisation {
|
|
161
|
+
if (!this._selectedOrganisation) {
|
|
162
|
+
throw new Error('Organisation context is required but not available') as OrganisationSecurityError;
|
|
163
|
+
}
|
|
164
|
+
return this._selectedOrganisation;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
isOrganisationSecure(): boolean {
|
|
168
|
+
return !!(this._selectedOrganisation && this.user);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getPrimaryOrganisation(): Organisation | null {
|
|
172
|
+
// Look for org_admin role first, then leader, then member
|
|
173
|
+
const rolePriority = ['org_admin', 'leader', 'member'];
|
|
174
|
+
|
|
175
|
+
for (const role of rolePriority) {
|
|
176
|
+
const membership = this._userMemberships.find((m: any) => m.role === role);
|
|
177
|
+
if (membership) {
|
|
178
|
+
return this._organisations.find((org: any) => org.id === membership.organisation_id) || null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
buildOrganisationHierarchy(orgs: Organisation[]): OrganisationHierarchy[] {
|
|
186
|
+
const orgMap = new Map<string, Organisation>();
|
|
187
|
+
orgs.forEach(org => orgMap.set(org.id, org));
|
|
188
|
+
|
|
189
|
+
const roots: OrganisationHierarchy[] = [];
|
|
190
|
+
|
|
191
|
+
orgs.forEach(org => {
|
|
192
|
+
if (!org.parent_id) {
|
|
193
|
+
// Root organisation
|
|
194
|
+
roots.push({
|
|
195
|
+
organisation: org,
|
|
196
|
+
children: [],
|
|
197
|
+
depth: 0
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// For now, return flat structure - hierarchy building can be added later
|
|
203
|
+
return roots;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Lifecycle methods
|
|
207
|
+
async initialize(): Promise<void> {
|
|
208
|
+
await super.initialize();
|
|
209
|
+
await this.loadUserOrganisations();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
cleanup(): void {
|
|
213
|
+
// Cleanup on unmount
|
|
214
|
+
this.isLoadingRef = false;
|
|
215
|
+
this.hasFailedRef = false;
|
|
216
|
+
this.lastLoadTimeRef = 0;
|
|
217
|
+
// Abort any pending requests
|
|
218
|
+
if (this.abortControllerRef) {
|
|
219
|
+
this.abortControllerRef.abort();
|
|
220
|
+
this.abortControllerRef = null;
|
|
221
|
+
}
|
|
222
|
+
// Reset state
|
|
223
|
+
this._selectedOrganisation = null;
|
|
224
|
+
this._organisations = [];
|
|
225
|
+
this._userMemberships = [];
|
|
226
|
+
this._roleMapState = new Map();
|
|
227
|
+
this._isLoading = false;
|
|
228
|
+
this._error = null;
|
|
229
|
+
this._isContextReady = false;
|
|
230
|
+
super.cleanup();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
protected async doInitialize(): Promise<void> {
|
|
234
|
+
// Initial setup
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
protected doCleanup(): void {
|
|
238
|
+
// Cleanup any resources
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async setDatabaseOrganisationContext(organisation: Organisation): Promise<void> {
|
|
242
|
+
if (!this.supabaseClient || !this.session) {
|
|
243
|
+
console.warn('[OrganisationService] No Supabase client or session available for setting organisation context');
|
|
244
|
+
this._isContextReady = false;
|
|
245
|
+
this.notify();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await setOrganisationContext(this.supabaseClient, organisation.id);
|
|
251
|
+
DebugLogger.log('OrganisationService', 'Database organisation context set to:', organisation.display_name);
|
|
252
|
+
this._isContextReady = true;
|
|
253
|
+
this.notify();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error('[OrganisationService] Failed to set database organisation context:', error);
|
|
256
|
+
this._isContextReady = false;
|
|
257
|
+
this.notify();
|
|
258
|
+
// Don't throw - this is a non-critical operation
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async loadUserOrganisations(): Promise<void> {
|
|
263
|
+
// Add call tracking to detect race conditions
|
|
264
|
+
const callId = Math.random().toString(36).substr(2, 9);
|
|
265
|
+
console.log(`[OrganisationService] Starting loadUserOrganisations call ${callId}`);
|
|
266
|
+
|
|
267
|
+
if (!this.user || !this.session || !this.supabaseClient) {
|
|
268
|
+
// Clear state when no user, session, or supabase client
|
|
269
|
+
DebugLogger.log('OrganisationService', 'Clearing organisation state - no user, session, or supabase client');
|
|
270
|
+
this._selectedOrganisation = null;
|
|
271
|
+
this._organisations = [];
|
|
272
|
+
this._userMemberships = [];
|
|
273
|
+
this._isLoading = false;
|
|
274
|
+
this._error = null;
|
|
275
|
+
this.notify();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Additional check to prevent loading during auth state changes
|
|
280
|
+
if (this.isLoadingRef) {
|
|
281
|
+
console.log("OrganisationService", "Already loading, skipping duplicate load");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Prevent rapid retries - minimum 2 seconds between attempts
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
if (now - this.lastLoadTimeRef < 2000) {
|
|
288
|
+
console.log("OrganisationService", "Too soon since last load, skipping");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Cancel any existing request
|
|
293
|
+
if (this.abortControllerRef) {
|
|
294
|
+
this.abortControllerRef.abort();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Create new abort controller for this request
|
|
298
|
+
this.abortControllerRef = new AbortController();
|
|
299
|
+
const abortSignal = this.abortControllerRef.signal;
|
|
300
|
+
|
|
301
|
+
this.lastLoadTimeRef = now;
|
|
302
|
+
this.isLoadingRef = true;
|
|
303
|
+
this._isLoading = true;
|
|
304
|
+
this._error = null;
|
|
305
|
+
this.notify();
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
DebugLogger.log("OrganisationService", "Loading organisations for user:", this.user.id);
|
|
309
|
+
|
|
310
|
+
// Debug: Log Supabase client configuration
|
|
311
|
+
console.log("[OrganisationService] Supabase client ready:", {
|
|
312
|
+
isConnected: !!this.supabaseClient,
|
|
313
|
+
hasAuth: !!this.supabaseClient.auth,
|
|
314
|
+
hasRpc: !!this.supabaseClient.rpc
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Get user's organisation memberships using secure RPC function
|
|
318
|
+
// Only get actual members (org_admin, leader, member) - exclude supporters
|
|
319
|
+
let memberships, membershipError;
|
|
320
|
+
try {
|
|
321
|
+
console.log("[OrganisationService] Making RPC call to data_user_organisation_roles_get...");
|
|
322
|
+
|
|
323
|
+
// Add timeout and abort signal to prevent hanging RPC calls
|
|
324
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
325
|
+
const timeoutId = setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 10000);
|
|
326
|
+
abortSignal.addEventListener('abort', () => {
|
|
327
|
+
clearTimeout(timeoutId);
|
|
328
|
+
reject(new Error('Request aborted'));
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const rpcPromise = this.supabaseClient.rpc('data_user_organisation_roles_get', {
|
|
333
|
+
p_user_id: this.user.id,
|
|
334
|
+
p_organisation_id: null
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Check if request was aborted before making the call
|
|
338
|
+
if (abortSignal.aborted) {
|
|
339
|
+
throw new Error('Request aborted');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const result = await Promise.race([rpcPromise, timeoutPromise]) as any;
|
|
343
|
+
|
|
344
|
+
console.log("[OrganisationService] RPC call completed:", {
|
|
345
|
+
hasData: !!result.data,
|
|
346
|
+
hasError: !!result.error,
|
|
347
|
+
dataLength: result.data?.length || 0,
|
|
348
|
+
errorMessage: result.error?.message || 'No error'
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Filter to only actual members (org_admin, leader, member) - exclude supporters
|
|
352
|
+
memberships = result.data?.filter((role: any) =>
|
|
353
|
+
['org_admin', 'leader', 'member'].includes(role.role)
|
|
354
|
+
) || [];
|
|
355
|
+
membershipError = result.error;
|
|
356
|
+
} catch (queryError: any) {
|
|
357
|
+
membershipError = queryError;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (membershipError) {
|
|
361
|
+
console.error("[OrganisationService] Error loading memberships:", membershipError);
|
|
362
|
+
|
|
363
|
+
// If RPC fails with timeout, try direct database query as fallback
|
|
364
|
+
if (membershipError.message?.includes('timeout')) {
|
|
365
|
+
console.log("[OrganisationService] RPC timed out, trying direct database query as fallback...");
|
|
366
|
+
try {
|
|
367
|
+
// Check if request was aborted before making fallback query
|
|
368
|
+
if (abortSignal.aborted) {
|
|
369
|
+
throw new Error('Request aborted');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const { data: fallbackData, error: fallbackError } = await this.supabaseClient
|
|
373
|
+
.from('rbac_organisation_roles')
|
|
374
|
+
.select(`
|
|
375
|
+
id,
|
|
376
|
+
user_id,
|
|
377
|
+
organisation_id,
|
|
378
|
+
role,
|
|
379
|
+
status,
|
|
380
|
+
granted_at,
|
|
381
|
+
granted_by,
|
|
382
|
+
revoked_at,
|
|
383
|
+
revoked_by,
|
|
384
|
+
notes,
|
|
385
|
+
created_at,
|
|
386
|
+
updated_at,
|
|
387
|
+
organisations!inner(
|
|
388
|
+
id,
|
|
389
|
+
name,
|
|
390
|
+
display_name,
|
|
391
|
+
subscription_tier,
|
|
392
|
+
settings,
|
|
393
|
+
is_active,
|
|
394
|
+
parent_id,
|
|
395
|
+
created_at,
|
|
396
|
+
updated_at
|
|
397
|
+
)
|
|
398
|
+
`)
|
|
399
|
+
.eq('user_id', this.user.id)
|
|
400
|
+
.eq('status', 'active')
|
|
401
|
+
.is('revoked_at', null)
|
|
402
|
+
.in('role', ['org_admin', 'leader', 'member']);
|
|
403
|
+
|
|
404
|
+
if (fallbackError) {
|
|
405
|
+
console.error("[OrganisationService] Fallback query also failed:", fallbackError);
|
|
406
|
+
throw membershipError; // Throw original error
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.log("[OrganisationService] Fallback query successful, got", fallbackData?.length || 0, "memberships");
|
|
410
|
+
memberships = fallbackData || [];
|
|
411
|
+
membershipError = null;
|
|
412
|
+
} catch (fallbackErr) {
|
|
413
|
+
console.error("[OrganisationService] Fallback query failed:", fallbackErr);
|
|
414
|
+
throw membershipError; // Throw original error
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
throw membershipError;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
DebugLogger.log("OrganisationService", "Raw memberships data:", memberships);
|
|
422
|
+
|
|
423
|
+
if (!memberships || memberships.length === 0) {
|
|
424
|
+
throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Get organisation details for the memberships
|
|
428
|
+
const organisationIds = memberships
|
|
429
|
+
.map((m: any) => m.organisation_id)
|
|
430
|
+
.filter((id: string) => {
|
|
431
|
+
// Better validation to prevent empty string UUID errors
|
|
432
|
+
if (!id || typeof id !== 'string') {
|
|
433
|
+
console.warn("[OrganisationService] Invalid organisation ID (not string):", id);
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
const trimmedId = id.trim();
|
|
437
|
+
if (trimmedId === '') {
|
|
438
|
+
console.warn("[OrganisationService] Empty organisation ID found");
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
// Validate UUID format
|
|
442
|
+
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);
|
|
443
|
+
if (!isValidUuid) {
|
|
444
|
+
console.warn("[OrganisationService] Invalid UUID format:", trimmedId);
|
|
445
|
+
}
|
|
446
|
+
return isValidUuid;
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (organisationIds.length === 0) {
|
|
450
|
+
console.warn("[OrganisationService] No valid organisation IDs found in memberships:", memberships);
|
|
451
|
+
throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
DebugLogger.log("OrganisationService", "Valid organisation IDs:", organisationIds);
|
|
455
|
+
|
|
456
|
+
// Check if request was aborted before making organisations query
|
|
457
|
+
if (abortSignal.aborted) {
|
|
458
|
+
throw new Error('Request aborted');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const { data: allOrganisations, error: orgError } = await this.supabaseClient
|
|
462
|
+
.from('organisations')
|
|
463
|
+
.select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at');
|
|
464
|
+
|
|
465
|
+
if (orgError) {
|
|
466
|
+
console.error("[OrganisationService] Error loading organisations:", orgError);
|
|
467
|
+
throw orgError;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Filter manually on the client side
|
|
471
|
+
const organisations = allOrganisations?.filter(org =>
|
|
472
|
+
organisationIds.includes(org.id)
|
|
473
|
+
) || [];
|
|
474
|
+
|
|
475
|
+
// Create a map of organisation_id to role from the memberships data
|
|
476
|
+
const roleMap = new Map<string, string>();
|
|
477
|
+
memberships?.forEach((membership: any) => {
|
|
478
|
+
roleMap.set(membership.organisation_id, membership.role);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Extract organisations and memberships
|
|
482
|
+
const orgs = organisations as Organisation[];
|
|
483
|
+
const activeOrgs = orgs.filter(org => org.is_active);
|
|
484
|
+
|
|
485
|
+
if (activeOrgs.length === 0) {
|
|
486
|
+
throw new Error('User has no access to active organisations') as OrganisationSecurityError;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
DebugLogger.log("OrganisationService", "Active organisations:", activeOrgs);
|
|
490
|
+
|
|
491
|
+
this._organisations = activeOrgs;
|
|
492
|
+
this._userMemberships = memberships as OrganisationMembership[];
|
|
493
|
+
|
|
494
|
+
// Store role map in component state for later use
|
|
495
|
+
this._roleMapState = roleMap;
|
|
496
|
+
|
|
497
|
+
// Auto-select organisation: try persisted, then primary, then first
|
|
498
|
+
let initialOrg: Organisation | null = null;
|
|
499
|
+
|
|
500
|
+
// 1. Try to restore from localStorage
|
|
501
|
+
try {
|
|
502
|
+
const persistedOrgString = localStorage.getItem('pace-core-selected-organisation');
|
|
503
|
+
if (persistedOrgString) {
|
|
504
|
+
const persistedOrg = JSON.parse(persistedOrgString) as Organisation;
|
|
505
|
+
// Validate persisted org ID before using it
|
|
506
|
+
if (persistedOrg.id && typeof persistedOrg.id === 'string' && persistedOrg.id.trim() !== '') {
|
|
507
|
+
const validPersistedOrg = activeOrgs.find(org => org.id === persistedOrg.id);
|
|
508
|
+
if (validPersistedOrg) {
|
|
509
|
+
initialOrg = validPersistedOrg;
|
|
510
|
+
DebugLogger.log("OrganisationService", "Restored persisted organisation:", initialOrg.display_name);
|
|
511
|
+
} else {
|
|
512
|
+
console.warn("[OrganisationService] Persisted organisation not found in active orgs, clearing cache");
|
|
513
|
+
localStorage.removeItem('pace-core-selected-organisation');
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
console.warn("[OrganisationService] Invalid persisted organisation ID, clearing cache");
|
|
517
|
+
localStorage.removeItem('pace-core-selected-organisation');
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch (storageError) {
|
|
521
|
+
console.warn("[OrganisationService] Failed to restore persisted organisation:", storageError);
|
|
522
|
+
// Clear potentially corrupted cache
|
|
523
|
+
localStorage.removeItem('pace-core-selected-organisation');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 2. Fall back to org_admin role organisation (highest privilege)
|
|
527
|
+
if (!initialOrg) {
|
|
528
|
+
const adminMembership = memberships.find((m: any) => m.role === 'org_admin');
|
|
529
|
+
if (adminMembership) {
|
|
530
|
+
const foundOrg = organisations.find((org: any) => org.id === adminMembership.organisation_id);
|
|
531
|
+
if (foundOrg) {
|
|
532
|
+
initialOrg = foundOrg;
|
|
533
|
+
DebugLogger.log("OrganisationService", "Selected org_admin organisation:", initialOrg.display_name);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 3. Fall back to first organisation
|
|
539
|
+
if (!initialOrg) {
|
|
540
|
+
initialOrg = activeOrgs[0];
|
|
541
|
+
DebugLogger.log("OrganisationService", "Selected first organisation:", initialOrg.display_name);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!initialOrg) {
|
|
545
|
+
throw new Error('No valid organisation found for user') as OrganisationSecurityError;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
this._selectedOrganisation = initialOrg;
|
|
549
|
+
|
|
550
|
+
// Persist selection
|
|
551
|
+
localStorage.setItem('pace-core-selected-organisation', JSON.stringify(initialOrg));
|
|
552
|
+
|
|
553
|
+
DebugLogger.log("OrganisationService", "Organisation context established:", {
|
|
554
|
+
selectedOrganisation: initialOrg.display_name,
|
|
555
|
+
totalOrganisations: activeOrgs.length,
|
|
556
|
+
userRole: roleMap.get(initialOrg.id)
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Reset retry count and failed flag on success
|
|
560
|
+
this.retryCount = 0;
|
|
561
|
+
this.hasFailedRef = false;
|
|
562
|
+
|
|
563
|
+
} catch (err) {
|
|
564
|
+
console.error("[OrganisationService] Failed to load organisations:", err);
|
|
565
|
+
this._error = err as Error;
|
|
566
|
+
// Increment retry count on error
|
|
567
|
+
this.retryCount = this.retryCount + 1;
|
|
568
|
+
// Set failed flag to prevent further attempts
|
|
569
|
+
this.hasFailedRef = true;
|
|
570
|
+
// Clear all cached data on error to prevent corruption
|
|
571
|
+
this.clearAllCachedData();
|
|
572
|
+
} finally {
|
|
573
|
+
// Always cleanup refs and abort controller
|
|
574
|
+
this.isLoadingRef = false;
|
|
575
|
+
this._isLoading = false;
|
|
576
|
+
this.abortControllerRef = null;
|
|
577
|
+
this.notify();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private clearAllCachedData(): void {
|
|
582
|
+
localStorage.removeItem('pace-core-selected-organisation');
|
|
583
|
+
localStorage.removeItem('pace-core-organisation-context');
|
|
584
|
+
this._selectedOrganisation = null;
|
|
585
|
+
this._organisations = [];
|
|
586
|
+
this._userMemberships = [];
|
|
587
|
+
this._roleMapState = new Map();
|
|
588
|
+
this.retryCount = 0;
|
|
589
|
+
this._isContextReady = false;
|
|
590
|
+
// Don't clear _error here - let it persist for error reporting
|
|
591
|
+
}
|
|
592
|
+
}
|