@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
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
getOrganisationFromEvent,
|
|
16
16
|
createScopeFromEvent,
|
|
17
17
|
isEventBasedScope,
|
|
18
|
-
isValidEventBasedScope
|
|
18
|
+
isValidEventBasedScope,
|
|
19
|
+
clearAllOrgDerivationCache,
|
|
20
|
+
clearOrgDerivationCache
|
|
19
21
|
} from '../eventContext';
|
|
20
22
|
|
|
21
23
|
// Mock Supabase client
|
|
@@ -26,8 +28,10 @@ const createMockSupabaseClient = () => {
|
|
|
26
28
|
single: vi.fn()
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
const fromMock = vi.fn().mockReturnValue(mockQuery);
|
|
32
|
+
|
|
29
33
|
return {
|
|
30
|
-
from:
|
|
34
|
+
from: fromMock,
|
|
31
35
|
query: mockQuery
|
|
32
36
|
} as unknown as SupabaseClient<Database>;
|
|
33
37
|
};
|
|
@@ -37,12 +41,23 @@ describe('Event Context Utilities', () => {
|
|
|
37
41
|
let mockQuery: any;
|
|
38
42
|
|
|
39
43
|
beforeEach(() => {
|
|
44
|
+
// Clear cache before each test
|
|
45
|
+
clearAllOrgDerivationCache();
|
|
46
|
+
|
|
40
47
|
mockSupabase = createMockSupabaseClient();
|
|
41
|
-
mockQuery
|
|
48
|
+
// Reset mockQuery to get a fresh query builder for each test
|
|
49
|
+
mockQuery = {
|
|
50
|
+
select: vi.fn().mockReturnThis(),
|
|
51
|
+
eq: vi.fn().mockReturnThis(),
|
|
52
|
+
single: vi.fn()
|
|
53
|
+
};
|
|
54
|
+
(mockSupabase.from as any).mockReturnValue(mockQuery);
|
|
42
55
|
});
|
|
43
56
|
|
|
44
57
|
afterEach(() => {
|
|
45
58
|
vi.clearAllMocks();
|
|
59
|
+
// Clear cache after each test
|
|
60
|
+
clearAllOrgDerivationCache();
|
|
46
61
|
});
|
|
47
62
|
|
|
48
63
|
describe('getOrganisationFromEvent', () => {
|
|
@@ -58,7 +73,7 @@ describe('Event Context Utilities', () => {
|
|
|
58
73
|
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
59
74
|
|
|
60
75
|
expect(result).toBe(organisationId);
|
|
61
|
-
expect(mockSupabase.from).toHaveBeenCalledWith('
|
|
76
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('core_events');
|
|
62
77
|
expect(mockQuery.select).toHaveBeenCalledWith('organisation_id');
|
|
63
78
|
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', eventId);
|
|
64
79
|
expect(mockQuery.single).toHaveBeenCalled();
|
|
@@ -80,6 +95,9 @@ describe('Event Context Utilities', () => {
|
|
|
80
95
|
it('should return null when data is null', async () => {
|
|
81
96
|
const eventId = 'event-123';
|
|
82
97
|
|
|
98
|
+
// Clear cache for this specific test
|
|
99
|
+
clearOrgDerivationCache(eventId);
|
|
100
|
+
|
|
83
101
|
mockQuery.single.mockResolvedValue({
|
|
84
102
|
data: null,
|
|
85
103
|
error: null
|
|
@@ -94,6 +112,9 @@ describe('Event Context Utilities', () => {
|
|
|
94
112
|
const eventId = 'event-123';
|
|
95
113
|
const dbError = new Error('Database connection failed');
|
|
96
114
|
|
|
115
|
+
// Clear cache for this specific test
|
|
116
|
+
clearOrgDerivationCache(eventId);
|
|
117
|
+
|
|
97
118
|
mockQuery.single.mockRejectedValue(dbError);
|
|
98
119
|
|
|
99
120
|
await expect(getOrganisationFromEvent(mockSupabase, eventId))
|
|
@@ -103,6 +124,9 @@ describe('Event Context Utilities', () => {
|
|
|
103
124
|
it('should handle empty organisation_id', async () => {
|
|
104
125
|
const eventId = 'event-123';
|
|
105
126
|
|
|
127
|
+
// Clear cache for this specific test
|
|
128
|
+
clearOrgDerivationCache(eventId);
|
|
129
|
+
|
|
106
130
|
mockQuery.single.mockResolvedValue({
|
|
107
131
|
data: { organisation_id: null },
|
|
108
132
|
error: null
|
|
@@ -168,6 +192,9 @@ describe('Event Context Utilities', () => {
|
|
|
168
192
|
it('should return null when organisation lookup fails', async () => {
|
|
169
193
|
const eventId = 'event-123';
|
|
170
194
|
|
|
195
|
+
// Clear cache for this specific test
|
|
196
|
+
clearOrgDerivationCache(eventId);
|
|
197
|
+
|
|
171
198
|
mockQuery.single.mockResolvedValue({
|
|
172
199
|
data: { organisation_id: null },
|
|
173
200
|
error: null
|
|
@@ -182,6 +209,9 @@ describe('Event Context Utilities', () => {
|
|
|
182
209
|
const eventId = 'event-123';
|
|
183
210
|
const dbError = new Error('Database connection failed');
|
|
184
211
|
|
|
212
|
+
// Clear cache for this specific test
|
|
213
|
+
clearOrgDerivationCache(eventId);
|
|
214
|
+
|
|
185
215
|
mockQuery.single.mockRejectedValue(dbError);
|
|
186
216
|
|
|
187
217
|
await expect(createScopeFromEvent(mockSupabase, eventId))
|
|
@@ -349,15 +379,31 @@ describe('Event Context Utilities', () => {
|
|
|
349
379
|
const organisationId1 = 'org-123';
|
|
350
380
|
const organisationId2 = 'org-456';
|
|
351
381
|
|
|
352
|
-
|
|
353
|
-
|
|
382
|
+
// Clear cache for these specific events
|
|
383
|
+
clearOrgDerivationCache(eventId1);
|
|
384
|
+
clearOrgDerivationCache(eventId2);
|
|
385
|
+
|
|
386
|
+
// Create separate query builders for concurrent calls
|
|
387
|
+
const mockQuery1 = {
|
|
388
|
+
select: vi.fn().mockReturnThis(),
|
|
389
|
+
eq: vi.fn().mockReturnThis(),
|
|
390
|
+
single: vi.fn().mockResolvedValue({
|
|
354
391
|
data: { organisation_id: organisationId1 },
|
|
355
392
|
error: null
|
|
356
393
|
})
|
|
357
|
-
|
|
394
|
+
};
|
|
395
|
+
const mockQuery2 = {
|
|
396
|
+
select: vi.fn().mockReturnThis(),
|
|
397
|
+
eq: vi.fn().mockReturnThis(),
|
|
398
|
+
single: vi.fn().mockResolvedValue({
|
|
358
399
|
data: { organisation_id: organisationId2 },
|
|
359
400
|
error: null
|
|
360
|
-
})
|
|
401
|
+
})
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
(mockSupabase.from as any)
|
|
405
|
+
.mockReturnValueOnce(mockQuery1)
|
|
406
|
+
.mockReturnValueOnce(mockQuery2);
|
|
361
407
|
|
|
362
408
|
const [result1, result2] = await Promise.all([
|
|
363
409
|
getOrganisationFromEvent(mockSupabase, eventId1),
|
|
@@ -376,15 +422,31 @@ describe('Event Context Utilities', () => {
|
|
|
376
422
|
const appId1 = 'app-123';
|
|
377
423
|
const appId2 = 'app-456';
|
|
378
424
|
|
|
379
|
-
|
|
380
|
-
|
|
425
|
+
// Clear cache for these specific events
|
|
426
|
+
clearOrgDerivationCache(eventId1);
|
|
427
|
+
clearOrgDerivationCache(eventId2);
|
|
428
|
+
|
|
429
|
+
// Create separate query builders for concurrent calls
|
|
430
|
+
const mockQuery1 = {
|
|
431
|
+
select: vi.fn().mockReturnThis(),
|
|
432
|
+
eq: vi.fn().mockReturnThis(),
|
|
433
|
+
single: vi.fn().mockResolvedValue({
|
|
381
434
|
data: { organisation_id: organisationId1 },
|
|
382
435
|
error: null
|
|
383
436
|
})
|
|
384
|
-
|
|
437
|
+
};
|
|
438
|
+
const mockQuery2 = {
|
|
439
|
+
select: vi.fn().mockReturnThis(),
|
|
440
|
+
eq: vi.fn().mockReturnThis(),
|
|
441
|
+
single: vi.fn().mockResolvedValue({
|
|
385
442
|
data: { organisation_id: organisationId2 },
|
|
386
443
|
error: null
|
|
387
|
-
})
|
|
444
|
+
})
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
(mockSupabase.from as any)
|
|
448
|
+
.mockReturnValueOnce(mockQuery1)
|
|
449
|
+
.mockReturnValueOnce(mockQuery2);
|
|
388
450
|
|
|
389
451
|
const [result1, result2] = await Promise.all([
|
|
390
452
|
createScopeFromEvent(mockSupabase, eventId1, appId1),
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Validator for RBAC
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/ContextValidator
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Centralized validation for RBAC context requirements based on app configuration.
|
|
8
|
+
* Enforces app-specific context rules with single primary context:
|
|
9
|
+
* - requires_event = TRUE: Event is PRIMARY context, org derived from event (org not required in input)
|
|
10
|
+
* - requires_event = FALSE: Organisation is PRIMARY context, event optional
|
|
11
|
+
* - PORTAL/ADMIN apps: Both contexts optional (allows users to view/edit own profiles, super admin access)
|
|
12
|
+
*
|
|
13
|
+
* Key principle: Only one primary context is required based on app config. The other is derived or optional.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
17
|
+
import { Database } from '../../types/database';
|
|
18
|
+
import { UUID, Scope } from '../types';
|
|
19
|
+
import { EventContextRequiredError, OrganisationContextRequiredError } from '../types';
|
|
20
|
+
import { getOrganisationFromEvent } from './eventContext';
|
|
21
|
+
import { createLogger } from '../../utils/core/logger';
|
|
22
|
+
|
|
23
|
+
const log = createLogger('ContextValidator');
|
|
24
|
+
|
|
25
|
+
export interface AppConfig {
|
|
26
|
+
requires_event: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if an app allows optional contexts (both organisation and event optional)
|
|
31
|
+
* @param appName - App name to check
|
|
32
|
+
* @returns True if app allows optional contexts
|
|
33
|
+
*/
|
|
34
|
+
function allowsOptionalContexts(appName?: string): boolean {
|
|
35
|
+
return appName === 'PORTAL' || appName === 'ADMIN';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ContextValidationResult {
|
|
39
|
+
isValid: boolean;
|
|
40
|
+
resolvedScope: Scope | null;
|
|
41
|
+
error: Error | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Context Validator class
|
|
46
|
+
*
|
|
47
|
+
* Validates and resolves RBAC scope based on app configuration requirements.
|
|
48
|
+
*/
|
|
49
|
+
export class ContextValidator {
|
|
50
|
+
/**
|
|
51
|
+
* Validate scope against app requirements
|
|
52
|
+
*
|
|
53
|
+
* @param scope - Current scope
|
|
54
|
+
* @param appConfig - App configuration (requires_event flag)
|
|
55
|
+
* @param appName - App name (for PORTAL/ADMIN special case)
|
|
56
|
+
* @returns Validation result with resolved scope
|
|
57
|
+
*/
|
|
58
|
+
static async validateScope(
|
|
59
|
+
scope: Scope,
|
|
60
|
+
appConfig: AppConfig | null,
|
|
61
|
+
appName?: string
|
|
62
|
+
): Promise<ContextValidationResult> {
|
|
63
|
+
// PORTAL/ADMIN special case: both contexts optional
|
|
64
|
+
if (allowsOptionalContexts(appName)) {
|
|
65
|
+
return {
|
|
66
|
+
isValid: true,
|
|
67
|
+
resolvedScope: {
|
|
68
|
+
organisationId: scope.organisationId,
|
|
69
|
+
eventId: scope.eventId,
|
|
70
|
+
appId: scope.appId
|
|
71
|
+
},
|
|
72
|
+
error: null
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If no app config, default to requiring org context
|
|
77
|
+
if (!appConfig) {
|
|
78
|
+
if (!scope.organisationId) {
|
|
79
|
+
return {
|
|
80
|
+
isValid: false,
|
|
81
|
+
resolvedScope: null,
|
|
82
|
+
error: new OrganisationContextRequiredError()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
isValid: true,
|
|
87
|
+
resolvedScope: scope,
|
|
88
|
+
error: null
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Event-required apps: must have eventId, derive org from event
|
|
93
|
+
if (appConfig.requires_event) {
|
|
94
|
+
if (!scope.eventId) {
|
|
95
|
+
return {
|
|
96
|
+
isValid: false,
|
|
97
|
+
resolvedScope: null,
|
|
98
|
+
error: new EventContextRequiredError()
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If org is not provided, we'll need to derive it from event
|
|
103
|
+
// But for validation, we just check that eventId exists
|
|
104
|
+
// The actual derivation happens in resolveRequiredContext
|
|
105
|
+
return {
|
|
106
|
+
isValid: true,
|
|
107
|
+
resolvedScope: scope,
|
|
108
|
+
error: null
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Org-required apps: must have organisationId, eventId optional
|
|
113
|
+
if (!scope.organisationId) {
|
|
114
|
+
return {
|
|
115
|
+
isValid: false,
|
|
116
|
+
resolvedScope: null,
|
|
117
|
+
error: new OrganisationContextRequiredError()
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
isValid: true,
|
|
123
|
+
resolvedScope: scope,
|
|
124
|
+
error: null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resolve required context and derive missing values
|
|
130
|
+
*
|
|
131
|
+
* @param scope - Current scope
|
|
132
|
+
* @param appConfig - App configuration
|
|
133
|
+
* @param appName - App name (for PORTAL/ADMIN special case)
|
|
134
|
+
* @param supabase - Supabase client (for deriving org from event)
|
|
135
|
+
* @returns Resolved scope with all required context
|
|
136
|
+
*/
|
|
137
|
+
static async resolveRequiredContext(
|
|
138
|
+
scope: Scope,
|
|
139
|
+
appConfig: AppConfig | null,
|
|
140
|
+
appName?: string,
|
|
141
|
+
supabase?: SupabaseClient<Database> | null
|
|
142
|
+
): Promise<ContextValidationResult> {
|
|
143
|
+
// PORTAL/ADMIN special case: both contexts optional
|
|
144
|
+
if (allowsOptionalContexts(appName)) {
|
|
145
|
+
return {
|
|
146
|
+
isValid: true,
|
|
147
|
+
resolvedScope: {
|
|
148
|
+
organisationId: scope.organisationId,
|
|
149
|
+
eventId: scope.eventId,
|
|
150
|
+
appId: scope.appId
|
|
151
|
+
},
|
|
152
|
+
error: null
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If no app config, default to requiring org context
|
|
157
|
+
if (!appConfig) {
|
|
158
|
+
if (!scope.organisationId) {
|
|
159
|
+
return {
|
|
160
|
+
isValid: false,
|
|
161
|
+
resolvedScope: null,
|
|
162
|
+
error: new OrganisationContextRequiredError()
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
isValid: true,
|
|
167
|
+
resolvedScope: scope,
|
|
168
|
+
error: null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Event-required apps: must have eventId, derive org from event
|
|
173
|
+
if (appConfig.requires_event) {
|
|
174
|
+
if (!scope.eventId) {
|
|
175
|
+
return {
|
|
176
|
+
isValid: false,
|
|
177
|
+
resolvedScope: null,
|
|
178
|
+
error: new EventContextRequiredError()
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Derive organisationId from event if not provided
|
|
183
|
+
let organisationId: UUID | undefined = scope.organisationId;
|
|
184
|
+
if (!organisationId && supabase && scope.eventId) {
|
|
185
|
+
try {
|
|
186
|
+
const derivedOrgId = await this.deriveOrgFromEvent(supabase, scope.eventId);
|
|
187
|
+
organisationId = derivedOrgId || undefined;
|
|
188
|
+
if (!organisationId) {
|
|
189
|
+
return {
|
|
190
|
+
isValid: false,
|
|
191
|
+
resolvedScope: null,
|
|
192
|
+
error: new Error('Could not resolve organisation from event context')
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
log.error('Failed to derive org from event:', error);
|
|
197
|
+
return {
|
|
198
|
+
isValid: false,
|
|
199
|
+
resolvedScope: null,
|
|
200
|
+
error: error instanceof Error ? error : new Error('Failed to derive organisation from event')
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
} else if (!organisationId) {
|
|
204
|
+
return {
|
|
205
|
+
isValid: false,
|
|
206
|
+
resolvedScope: null,
|
|
207
|
+
error: new Error('Event context requires organisationId but it could not be derived (supabase client not available)')
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
isValid: true,
|
|
213
|
+
resolvedScope: {
|
|
214
|
+
organisationId,
|
|
215
|
+
eventId: scope.eventId,
|
|
216
|
+
appId: scope.appId
|
|
217
|
+
},
|
|
218
|
+
error: null
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Org-required apps: must have organisationId, eventId optional
|
|
223
|
+
if (!scope.organisationId) {
|
|
224
|
+
return {
|
|
225
|
+
isValid: false,
|
|
226
|
+
resolvedScope: null,
|
|
227
|
+
error: new OrganisationContextRequiredError()
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
isValid: true,
|
|
233
|
+
resolvedScope: scope,
|
|
234
|
+
error: null
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Derive organisation ID from event ID
|
|
240
|
+
*
|
|
241
|
+
* @param supabase - Supabase client
|
|
242
|
+
* @param eventId - Event ID
|
|
243
|
+
* @returns Organisation ID or null
|
|
244
|
+
*/
|
|
245
|
+
static async deriveOrgFromEvent(
|
|
246
|
+
supabase: SupabaseClient<Database>,
|
|
247
|
+
eventId: string
|
|
248
|
+
): Promise<UUID | null> {
|
|
249
|
+
return getOrganisationFromEvent(supabase, eventId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if context is ready for permission checks
|
|
254
|
+
*
|
|
255
|
+
* @param scope - Current scope
|
|
256
|
+
* @param appConfig - App configuration
|
|
257
|
+
* @param appName - App name
|
|
258
|
+
* @param hasSelectedEvent - Whether event is selected
|
|
259
|
+
* @param hasSelectedOrganisation - Whether organisation is selected
|
|
260
|
+
* @returns True if context is ready
|
|
261
|
+
*/
|
|
262
|
+
static isContextReady(
|
|
263
|
+
scope: Scope,
|
|
264
|
+
appConfig: AppConfig | null,
|
|
265
|
+
appName?: string,
|
|
266
|
+
hasSelectedEvent?: boolean,
|
|
267
|
+
hasSelectedOrganisation?: boolean
|
|
268
|
+
): boolean {
|
|
269
|
+
// PORTAL/ADMIN special case: context is always ready
|
|
270
|
+
if (allowsOptionalContexts(appName)) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// If no app config, default to requiring org context
|
|
275
|
+
if (!appConfig) {
|
|
276
|
+
return !!hasSelectedOrganisation || !!scope.organisationId;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Event-required apps: need event context
|
|
280
|
+
if (appConfig.requires_event) {
|
|
281
|
+
return !!hasSelectedEvent || !!scope.eventId;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Org-required apps: need org context
|
|
285
|
+
return !!hasSelectedOrganisation || !!scope.organisationId;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
@@ -12,9 +12,35 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
|
|
12
12
|
import { Database } from '../../types/database';
|
|
13
13
|
import { UUID, Scope } from '../types';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Cache for organisation derivation from event
|
|
17
|
+
* Key: eventId, Value: organisationId | null
|
|
18
|
+
* Maximum cache size to prevent memory leaks
|
|
19
|
+
*/
|
|
20
|
+
const orgDerivationCache = new Map<string, UUID | null>();
|
|
21
|
+
const MAX_CACHE_SIZE = 100; // Limit cache to 100 entries
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clear cache entry for a specific event (useful if event's org changes)
|
|
25
|
+
* @param eventId - Event ID to clear from cache
|
|
26
|
+
*/
|
|
27
|
+
export function clearOrgDerivationCache(eventId: string): void {
|
|
28
|
+
orgDerivationCache.delete(eventId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clear all cached organisation derivations
|
|
33
|
+
*/
|
|
34
|
+
export function clearAllOrgDerivationCache(): void {
|
|
35
|
+
orgDerivationCache.clear();
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
/**
|
|
16
39
|
* Get organization ID from event ID
|
|
17
40
|
*
|
|
41
|
+
* Uses caching to avoid repeated database queries for the same event.
|
|
42
|
+
* Cache is limited to prevent memory leaks.
|
|
43
|
+
*
|
|
18
44
|
* @param supabase - Supabase client
|
|
19
45
|
* @param eventId - Event ID
|
|
20
46
|
* @returns Promise resolving to organization ID or null
|
|
@@ -23,17 +49,40 @@ export async function getOrganisationFromEvent(
|
|
|
23
49
|
supabase: SupabaseClient<Database>,
|
|
24
50
|
eventId: string
|
|
25
51
|
): Promise<UUID | null> {
|
|
52
|
+
// Check cache first
|
|
53
|
+
if (orgDerivationCache.has(eventId)) {
|
|
54
|
+
return orgDerivationCache.get(eventId) ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Query database
|
|
26
58
|
const { data, error } = await supabase
|
|
27
|
-
.from('
|
|
59
|
+
.from('core_events')
|
|
28
60
|
.select('organisation_id')
|
|
29
61
|
.eq('event_id', eventId)
|
|
30
62
|
.single() as { data: { organisation_id: string } | null; error: any };
|
|
31
63
|
|
|
64
|
+
let organisationId: UUID | null = null;
|
|
65
|
+
|
|
32
66
|
if (error || !data) {
|
|
33
|
-
|
|
67
|
+
organisationId = null;
|
|
68
|
+
} else if (data.organisation_id) {
|
|
69
|
+
organisationId = data.organisation_id;
|
|
70
|
+
} else {
|
|
71
|
+
// organisation_id is null or undefined
|
|
72
|
+
organisationId = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Cache the result (with size limit to prevent memory leaks)
|
|
76
|
+
if (orgDerivationCache.size >= MAX_CACHE_SIZE) {
|
|
77
|
+
// Remove oldest entry (first key in Map)
|
|
78
|
+
const firstKey = orgDerivationCache.keys().next().value;
|
|
79
|
+
if (firstKey) {
|
|
80
|
+
orgDerivationCache.delete(firstKey);
|
|
81
|
+
}
|
|
34
82
|
}
|
|
83
|
+
orgDerivationCache.set(eventId, organisationId);
|
|
35
84
|
|
|
36
|
-
return
|
|
85
|
+
return organisationId;
|
|
37
86
|
}
|
|
38
87
|
|
|
39
88
|
/**
|
|
@@ -299,7 +299,15 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
299
299
|
// Lifecycle methods
|
|
300
300
|
async initialize(): Promise<void> {
|
|
301
301
|
await super.initialize();
|
|
302
|
+
// Set loading to true before starting session restoration
|
|
303
|
+
// This ensures ProtectedRoute shows loading state while session is being restored
|
|
304
|
+
this.authLoading = true;
|
|
305
|
+
this.notify();
|
|
306
|
+
|
|
307
|
+
// Setup auth state listener first - this will receive INITIAL_SESSION event
|
|
302
308
|
await this.setupAuthStateListener();
|
|
309
|
+
|
|
310
|
+
// Then restore session - this will trigger INITIAL_SESSION event if session exists
|
|
303
311
|
await this.restoreSession();
|
|
304
312
|
}
|
|
305
313
|
|
|
@@ -431,17 +439,25 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
431
439
|
this.sessionRestorationState.restorationError ||
|
|
432
440
|
(hasTimeoutError && session)) {
|
|
433
441
|
this.finishSessionRestoration();
|
|
434
|
-
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
// No session in INITIAL_SESSION event - user is not authenticated
|
|
445
|
+
// Finish restoration to clear loading state
|
|
446
|
+
if (this.sessionRestorationState.isRestoring) {
|
|
447
|
+
this.finishSessionRestoration();
|
|
435
448
|
}
|
|
436
449
|
}
|
|
437
450
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
451
|
+
// CRITICAL: Set loading to false AFTER handling INITIAL_SESSION
|
|
452
|
+
// This ensures ProtectedRoute waits for session restoration to complete
|
|
453
|
+
// before checking authentication state
|
|
454
|
+
this.authLoading = false;
|
|
455
|
+
this.notify();
|
|
456
|
+
return; // Return early to avoid setting loading to false again below
|
|
442
457
|
}
|
|
443
458
|
|
|
444
|
-
//
|
|
459
|
+
// For other events (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED), set loading to false
|
|
460
|
+
// INITIAL_SESSION is handled above and returns early
|
|
445
461
|
this.authLoading = false;
|
|
446
462
|
this.notify();
|
|
447
463
|
} catch (error) {
|
|
@@ -519,8 +535,21 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
519
535
|
this.authError = null;
|
|
520
536
|
}
|
|
521
537
|
|
|
522
|
-
//
|
|
523
|
-
|
|
538
|
+
// CRITICAL FIX: Don't finish restoration here - wait for INITIAL_SESSION event
|
|
539
|
+
// The INITIAL_SESSION event should fire when the auth state listener is set up
|
|
540
|
+
// However, if it doesn't fire within a short delay (e.g., edge case), we'll finish it
|
|
541
|
+
// This ensures ProtectedRoute waits for the event before checking auth state
|
|
542
|
+
|
|
543
|
+
// Set a short fallback timeout to finish restoration if INITIAL_SESSION doesn't fire
|
|
544
|
+
// This handles edge cases where the event might be delayed or not fire
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
// Only finish if restoration is still in progress and INITIAL_SESSION hasn't fired
|
|
547
|
+
// The INITIAL_SESSION handler will have set restorationComplete to true if it fired
|
|
548
|
+
if (this.sessionRestorationState.isRestoring && !this.sessionRestorationState.restorationComplete) {
|
|
549
|
+
logger.debug('AuthService', 'INITIAL_SESSION event did not fire, finishing restoration');
|
|
550
|
+
this.finishSessionRestoration();
|
|
551
|
+
}
|
|
552
|
+
}, 100); // 100ms fallback - INITIAL_SESSION should fire immediately when listener is set up
|
|
524
553
|
} catch (error) {
|
|
525
554
|
const restorationError = error instanceof Error
|
|
526
555
|
? error
|