@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
|
@@ -9,16 +9,27 @@
|
|
|
9
9
|
* consistent scope resolution logic.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { useEffect, useState, useRef } from 'react';
|
|
12
|
+
import { useEffect, useState, useRef, useMemo } from 'react';
|
|
13
13
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
14
14
|
import type { Database } from '../../types/database';
|
|
15
15
|
import type { Scope } from '../types';
|
|
16
|
-
import {
|
|
16
|
+
import { ContextValidator } from '../utils/contextValidator';
|
|
17
|
+
import type { AppConfig } from '../utils/contextValidator';
|
|
17
18
|
import { getCurrentAppName } from '../../utils/app/appNameResolver';
|
|
18
19
|
import { createLogger } from '../../utils/core/logger';
|
|
19
20
|
|
|
20
21
|
const log = createLogger('useResolvedScope');
|
|
21
22
|
|
|
23
|
+
// Cache app config to avoid repeated database queries
|
|
24
|
+
// App config rarely changes during a session, so we can cache it
|
|
25
|
+
const appConfigCache = new Map<string, { appId: string; appConfig: AppConfig; timestamp: number }>();
|
|
26
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
27
|
+
|
|
28
|
+
// Export function to clear cache (for testing)
|
|
29
|
+
export function clearAppConfigCache(): void {
|
|
30
|
+
appConfigCache.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
export interface UseResolvedScopeOptions {
|
|
23
34
|
/** Supabase client instance */
|
|
24
35
|
supabase: SupabaseClient<Database> | null;
|
|
@@ -78,11 +89,12 @@ export function useResolvedScope({
|
|
|
78
89
|
});
|
|
79
90
|
|
|
80
91
|
// Update stable scope ref in useEffect to avoid updates during render
|
|
92
|
+
// For PORTAL, allow scopes without organisationId
|
|
81
93
|
useEffect(() => {
|
|
82
|
-
if (resolvedScope
|
|
94
|
+
if (resolvedScope) {
|
|
83
95
|
const newScope = {
|
|
84
|
-
organisationId: resolvedScope.organisationId,
|
|
85
|
-
appId: resolvedScope.appId,
|
|
96
|
+
organisationId: resolvedScope.organisationId || '',
|
|
97
|
+
appId: resolvedScope.appId || '',
|
|
86
98
|
eventId: resolvedScope.eventId
|
|
87
99
|
};
|
|
88
100
|
|
|
@@ -92,16 +104,19 @@ export function useResolvedScope({
|
|
|
92
104
|
stableScopeRef.current.appId !== newScope.appId) {
|
|
93
105
|
stableScopeRef.current = {
|
|
94
106
|
organisationId: newScope.organisationId,
|
|
95
|
-
appId: newScope.appId
|
|
107
|
+
appId: newScope.appId,
|
|
96
108
|
eventId: newScope.eventId
|
|
97
109
|
};
|
|
98
110
|
}
|
|
99
|
-
} else
|
|
111
|
+
} else {
|
|
100
112
|
// Reset to empty scope when no resolved scope
|
|
101
113
|
stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
|
|
102
114
|
}
|
|
103
115
|
}, [resolvedScope]);
|
|
104
116
|
|
|
117
|
+
// Get app name to check if it's PORTAL (needed for return logic)
|
|
118
|
+
const appName = getCurrentAppName();
|
|
119
|
+
|
|
105
120
|
const stableScope = stableScopeRef.current;
|
|
106
121
|
|
|
107
122
|
useEffect(() => {
|
|
@@ -123,20 +138,28 @@ export function useResolvedScope({
|
|
|
123
138
|
setError(null);
|
|
124
139
|
|
|
125
140
|
try {
|
|
126
|
-
// Get app
|
|
141
|
+
// Get app name and config
|
|
142
|
+
const appName = getCurrentAppName();
|
|
127
143
|
let appId: string | undefined = undefined;
|
|
144
|
+
let appConfig: AppConfig | null = null;
|
|
128
145
|
|
|
129
|
-
// Try to resolve from database
|
|
130
|
-
if (supabase) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
// Try to resolve app config from database (with caching)
|
|
147
|
+
if (supabase && appName) {
|
|
148
|
+
try {
|
|
149
|
+
// Check cache first
|
|
150
|
+
const cached = appConfigCache.get(appName);
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
if (cached && (now - cached.timestamp) < CACHE_TTL) {
|
|
153
|
+
appId = cached.appId;
|
|
154
|
+
appConfig = cached.appConfig;
|
|
155
|
+
} else {
|
|
156
|
+
// Cache miss or expired - fetch from database
|
|
134
157
|
const { data: app, error } = await supabase
|
|
135
158
|
.from('rbac_apps')
|
|
136
|
-
.select('id, name, is_active')
|
|
159
|
+
.select('id, name, requires_event, is_active')
|
|
137
160
|
.eq('name', appName)
|
|
138
161
|
.eq('is_active', true)
|
|
139
|
-
.single() as { data: { id: string; name: string; is_active: boolean } | null; error: any };
|
|
162
|
+
.single() as { data: { id: string; name: string; requires_event: boolean; is_active: boolean } | null; error: any };
|
|
140
163
|
|
|
141
164
|
if (error) {
|
|
142
165
|
// Check if app exists but is inactive
|
|
@@ -148,83 +171,89 @@ export function useResolvedScope({
|
|
|
148
171
|
|
|
149
172
|
if (inactiveApp) {
|
|
150
173
|
log.error(`App "${appName}" exists but is inactive (is_active: ${inactiveApp.is_active})`);
|
|
174
|
+
// Don't cache inactive apps - set appId to undefined
|
|
175
|
+
appId = undefined;
|
|
151
176
|
} else {
|
|
152
177
|
log.error(`App "${appName}" not found in rbac_apps table`);
|
|
178
|
+
// Don't cache missing apps - set appId to undefined
|
|
179
|
+
appId = undefined;
|
|
153
180
|
}
|
|
154
181
|
} else if (app) {
|
|
155
182
|
appId = app.id;
|
|
183
|
+
appConfig = { requires_event: app.requires_event ?? false };
|
|
184
|
+
// Only cache successful lookups of active apps
|
|
185
|
+
appConfigCache.set(appName, { appId, appConfig, timestamp: now });
|
|
156
186
|
}
|
|
157
|
-
} catch (error) {
|
|
158
|
-
log.error('Unexpected error resolving app ID:', error);
|
|
159
187
|
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
log.error('Unexpected error resolving app config:', error);
|
|
160
190
|
}
|
|
161
191
|
}
|
|
162
192
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
appId: appId
|
|
172
|
-
});
|
|
173
|
-
setIsLoading(false);
|
|
174
|
-
}
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
193
|
+
// Build initial scope from available context
|
|
194
|
+
// For event-required apps: Only use eventId (org will be derived)
|
|
195
|
+
// For org-required apps: Only use organisationId (event is optional)
|
|
196
|
+
const initialScope: Scope = {
|
|
197
|
+
organisationId: appConfig?.requires_event ? undefined : (selectedOrganisationId || undefined),
|
|
198
|
+
eventId: selectedEventId || undefined,
|
|
199
|
+
appId: appId
|
|
200
|
+
};
|
|
177
201
|
|
|
178
|
-
//
|
|
179
|
-
if
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
setIsLoading(false);
|
|
187
|
-
}
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
202
|
+
// Use ContextValidator to resolve required context
|
|
203
|
+
// For PORTAL, always allow scope resolution even if appConfig is null
|
|
204
|
+
const validation = await ContextValidator.resolveRequiredContext(
|
|
205
|
+
initialScope,
|
|
206
|
+
appConfig,
|
|
207
|
+
appName || undefined,
|
|
208
|
+
supabase
|
|
209
|
+
);
|
|
190
210
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const eventScope = await createScopeFromEvent(supabase, selectedEventId, appId);
|
|
195
|
-
if (!eventScope) {
|
|
196
|
-
log.error('Could not resolve organization from event context');
|
|
197
|
-
if (!cancelled) {
|
|
198
|
-
setResolvedScope(null);
|
|
199
|
-
setError(new Error('Could not resolve organisation from event context'));
|
|
200
|
-
setIsLoading(false);
|
|
201
|
-
}
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
// Preserve the resolved app ID
|
|
211
|
+
if (!validation.isValid) {
|
|
212
|
+
// For PORTAL/ADMIN apps, allow scope without org/event even if validation fails
|
|
213
|
+
if (appName === 'PORTAL' || appName === 'ADMIN') {
|
|
205
214
|
if (!cancelled) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
// For PORTAL/ADMIN, we need at least an appId. If we don't have it from the query,
|
|
216
|
+
// we'll set it to undefined and let the component handle it (it can use contextAppId)
|
|
217
|
+
const optionalContextScope: Scope = {
|
|
218
|
+
organisationId: undefined,
|
|
219
|
+
eventId: undefined,
|
|
220
|
+
appId: appId || undefined // appId might be undefined if query failed, that's OK
|
|
221
|
+
};
|
|
222
|
+
setResolvedScope(optionalContextScope);
|
|
223
|
+
setError(null);
|
|
210
224
|
setIsLoading(false);
|
|
211
225
|
}
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// For event-required apps: if validation fails but we have an eventId, return scope with eventId
|
|
230
|
+
// The organisation will be derived later during permission checks
|
|
231
|
+
if (appConfig?.requires_event && selectedEventId) {
|
|
214
232
|
if (!cancelled) {
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
const eventScope: Scope = {
|
|
234
|
+
organisationId: undefined, // Will be derived from event during permission check
|
|
235
|
+
eventId: selectedEventId,
|
|
236
|
+
appId: appId || undefined
|
|
237
|
+
};
|
|
238
|
+
setResolvedScope(eventScope);
|
|
239
|
+
setError(null); // Don't set error - let permission check handle derivation
|
|
217
240
|
setIsLoading(false);
|
|
218
241
|
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!cancelled) {
|
|
246
|
+
setResolvedScope(null);
|
|
247
|
+
setError(validation.error || new Error('Context validation failed'));
|
|
248
|
+
setIsLoading(false);
|
|
219
249
|
}
|
|
220
250
|
return;
|
|
221
251
|
}
|
|
222
252
|
|
|
223
|
-
//
|
|
224
|
-
log.error('No organisation or event context available');
|
|
253
|
+
// Set resolved scope
|
|
225
254
|
if (!cancelled) {
|
|
226
|
-
setResolvedScope(
|
|
227
|
-
setError(
|
|
255
|
+
setResolvedScope(validation.resolvedScope);
|
|
256
|
+
setError(null);
|
|
228
257
|
setIsLoading(false);
|
|
229
258
|
}
|
|
230
259
|
} catch (err) {
|
|
@@ -242,8 +271,37 @@ export function useResolvedScope({
|
|
|
242
271
|
};
|
|
243
272
|
}, [selectedOrganisationId, selectedEventId, supabase]);
|
|
244
273
|
|
|
274
|
+
// Return scope if it has appId (for PORTAL/ADMIN) or organisationId (for other apps)
|
|
275
|
+
// For PORTAL/ADMIN, always return a scope (even if empty) so components can use contextAppId
|
|
276
|
+
const allowsOptionalContexts = appName === 'PORTAL' || appName === 'ADMIN';
|
|
277
|
+
const hasValidScope = allowsOptionalContexts
|
|
278
|
+
? true // PORTAL/ADMIN always have valid scope (even without org/event/appId)
|
|
279
|
+
: (stableScope.appId || stableScope.organisationId);
|
|
280
|
+
|
|
281
|
+
// CRITICAL FIX: Memoize finalScope to prevent creating new object references on every render
|
|
282
|
+
// This prevents infinite loops in useCan and other hooks that depend on scope equality
|
|
283
|
+
const finalScope: Scope | null = useMemo(() => {
|
|
284
|
+
if (!hasValidScope) {
|
|
285
|
+
return allowsOptionalContexts ? {} : null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Build scope object only with defined values to ensure stable reference
|
|
289
|
+
const scope: Scope = {};
|
|
290
|
+
if (stableScope.organisationId) {
|
|
291
|
+
scope.organisationId = stableScope.organisationId;
|
|
292
|
+
}
|
|
293
|
+
if (stableScope.eventId) {
|
|
294
|
+
scope.eventId = stableScope.eventId;
|
|
295
|
+
}
|
|
296
|
+
if (stableScope.appId) {
|
|
297
|
+
scope.appId = stableScope.appId;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return scope;
|
|
301
|
+
}, [hasValidScope, allowsOptionalContexts, stableScope.organisationId, stableScope.eventId, stableScope.appId]);
|
|
302
|
+
|
|
245
303
|
return {
|
|
246
|
-
resolvedScope:
|
|
304
|
+
resolvedScope: finalScope,
|
|
247
305
|
isLoading,
|
|
248
306
|
error
|
|
249
307
|
};
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
*
|
|
96
96
|
* - Must be used within `UnifiedAuthProvider` context
|
|
97
97
|
* - Requires `useOrganisations` and `useEvents` hooks to be available
|
|
98
|
-
* - Environment variables `VITE_SUPABASE_URL` and `
|
|
98
|
+
* - Environment variables `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY` must be set
|
|
99
99
|
* (or `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` for Next.js)
|
|
100
100
|
*
|
|
101
101
|
* ## See Also
|
|
@@ -120,6 +120,7 @@ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
|
120
120
|
import { useOrganisations } from '../../hooks/useOrganisations';
|
|
121
121
|
import { useEvents } from '../../hooks/useEvents';
|
|
122
122
|
import { useResolvedScope } from './useResolvedScope';
|
|
123
|
+
import { useSuperAdminBypass } from './useSuperAdminBypass';
|
|
123
124
|
import { createSecureClient, SecureSupabaseClient } from '../secureClient';
|
|
124
125
|
import type { Database } from '../../types/database';
|
|
125
126
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
@@ -134,13 +135,15 @@ const MAX_CACHE_SIZE = 5;
|
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
137
|
* Get cache key for secure client based on context
|
|
138
|
+
* CRITICAL: Must include isSuperAdmin in cache key to ensure correct filtering behavior
|
|
137
139
|
*/
|
|
138
140
|
function getCacheKey(
|
|
139
141
|
organisationId: string,
|
|
140
142
|
eventId: string | undefined,
|
|
141
|
-
appId: string | undefined
|
|
143
|
+
appId: string | undefined,
|
|
144
|
+
isSuperAdmin: boolean
|
|
142
145
|
): string {
|
|
143
|
-
return `${organisationId}-${eventId || 'no-event'}-${appId || 'no-app'}`;
|
|
146
|
+
return `${organisationId}-${eventId || 'no-event'}-${appId || 'no-app'}-${isSuperAdmin ? 'super' : 'regular'}`;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
/**
|
|
@@ -162,8 +165,8 @@ function getSupabaseConfig(): { url: string; key: string } | null {
|
|
|
162
165
|
getEnvVar('NEXT_PUBLIC_SUPABASE_URL') ||
|
|
163
166
|
null;
|
|
164
167
|
|
|
165
|
-
const supabaseKey = getEnvVar('
|
|
166
|
-
getEnvVar('
|
|
168
|
+
const supabaseKey = getEnvVar('VITE_SUPABASE_PUBLISHABLE_KEY') ||
|
|
169
|
+
getEnvVar('NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY') ||
|
|
167
170
|
null;
|
|
168
171
|
|
|
169
172
|
if (!supabaseUrl || !supabaseKey) {
|
|
@@ -210,6 +213,16 @@ export function useSecureSupabase(
|
|
|
210
213
|
const eventsContext = useEvents();
|
|
211
214
|
const { selectedEvent } = eventsContext;
|
|
212
215
|
const eventLoading = 'eventLoading' in eventsContext ? eventsContext.eventLoading : false;
|
|
216
|
+
|
|
217
|
+
// Check super admin status for conditional filtering
|
|
218
|
+
// Use both verified status and metadata hint to avoid race conditions
|
|
219
|
+
// Strategy: Use verified status if available, otherwise use metadata hint during verification
|
|
220
|
+
// This prevents creating clients with wrong super admin status before verification completes
|
|
221
|
+
const { isSuperAdmin: verifiedIsSuperAdmin, isLoading: isVerifyingSuperAdmin } = useSuperAdminBypass();
|
|
222
|
+
const metadataHint = Boolean(user?.app_metadata?.is_super_admin) || Boolean(user?.user_metadata?.is_super_admin);
|
|
223
|
+
// If verified as super admin, use that. If verification in progress, use metadata hint optimistically.
|
|
224
|
+
// Once verification completes and user is not super admin, verifiedIsSuperAdmin will be false.
|
|
225
|
+
const isSuperAdmin = verifiedIsSuperAdmin || (isVerifyingSuperAdmin && metadataHint);
|
|
213
226
|
|
|
214
227
|
// Resolve scope to get appId
|
|
215
228
|
const { resolvedScope } = useResolvedScope({
|
|
@@ -235,19 +248,20 @@ export function useSecureSupabase(
|
|
|
235
248
|
return baseClient || authSupabase || null;
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
251
|
+
// Use resolved scope to get organisationId (derived from event if needed)
|
|
252
|
+
// For event-required apps, resolvedScope.organisationId is derived from event
|
|
253
|
+
// For org-required apps, resolvedScope.organisationId comes from selectedOrganisation
|
|
254
|
+
const organisationId = resolvedScope?.organisationId;
|
|
255
|
+
const eventId = resolvedScope?.eventId || selectedEvent?.event_id;
|
|
256
|
+
const appId = resolvedScope?.appId;
|
|
257
|
+
|
|
258
|
+
// If we have organisation context (from resolved scope), create or reuse a secure client
|
|
259
|
+
if (organisationId && user?.id) {
|
|
246
260
|
// Update previous context
|
|
247
261
|
prevContextRef.current = { organisationId, eventId, appId };
|
|
248
262
|
|
|
249
|
-
// Check cache first
|
|
250
|
-
const cacheKey = getCacheKey(organisationId, eventId, appId);
|
|
263
|
+
// Check cache first (must include isSuperAdmin in key for correct filtering)
|
|
264
|
+
const cacheKey = getCacheKey(organisationId, eventId, appId, isSuperAdmin);
|
|
251
265
|
const cachedClient = secureClientCache.get(cacheKey);
|
|
252
266
|
|
|
253
267
|
if (cachedClient) {
|
|
@@ -259,7 +273,7 @@ export function useSecureSupabase(
|
|
|
259
273
|
const config = getSupabaseConfig();
|
|
260
274
|
if (!config || !config.url || !config.key) {
|
|
261
275
|
logger.warn('useSecureSupabase', 'Missing Supabase environment variables. Falling back to base client.', {
|
|
262
|
-
note: 'Ensure VITE_SUPABASE_URL and
|
|
276
|
+
note: 'Ensure VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY are set in your environment.'
|
|
263
277
|
});
|
|
264
278
|
return baseClient || authSupabase || null;
|
|
265
279
|
}
|
|
@@ -270,7 +284,8 @@ export function useSecureSupabase(
|
|
|
270
284
|
config.key,
|
|
271
285
|
organisationId as any, // organisationId is string, UUID is string alias
|
|
272
286
|
eventId,
|
|
273
|
-
appId as any // appId is string | undefined, UUID is string alias
|
|
287
|
+
appId as any, // appId is string | undefined, UUID is string alias
|
|
288
|
+
isSuperAdmin // Pass super admin status for conditional filtering
|
|
274
289
|
);
|
|
275
290
|
|
|
276
291
|
// Cache the client for reuse
|
|
@@ -296,11 +311,13 @@ export function useSecureSupabase(
|
|
|
296
311
|
// Fallback to base client when context is not available
|
|
297
312
|
return baseClient || authSupabase || null;
|
|
298
313
|
}, [
|
|
299
|
-
|
|
314
|
+
resolvedScope?.organisationId,
|
|
315
|
+
resolvedScope?.eventId,
|
|
316
|
+
resolvedScope?.appId,
|
|
300
317
|
selectedEvent?.event_id,
|
|
301
318
|
user?.id,
|
|
302
319
|
eventLoading,
|
|
303
|
-
|
|
320
|
+
isSuperAdmin,
|
|
304
321
|
baseClient,
|
|
305
322
|
authSupabase
|
|
306
323
|
]);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useSuperAdminBypass
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
*
|
|
5
|
+
* Detects whether the current user is a super admin, keeps the
|
|
6
|
+
* server session override flag in sync, and exposes a boolean
|
|
7
|
+
* that downstream hooks can use to bypass organisation scoping.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
+
import { useUnifiedAuth, type UnifiedAuthContextType } from '../../providers/services/UnifiedAuthProvider';
|
|
12
|
+
import { isSuperAdmin as fetchIsSuperAdmin } from '../api';
|
|
13
|
+
import { setSuperAdminOverrideFlag } from '../../utils/context/superAdminOverride';
|
|
14
|
+
import { createLogger } from '../../utils/core/logger';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('useSuperAdminBypass');
|
|
17
|
+
|
|
18
|
+
export interface SuperAdminBypassState {
|
|
19
|
+
/** True when the user has been verified as a super admin */
|
|
20
|
+
isSuperAdmin: boolean;
|
|
21
|
+
/** True while the hook is checking the server */
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
/** Error returned by the verification request, if any */
|
|
24
|
+
error: Error | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useSafeUnifiedAuth(): UnifiedAuthContextType | null {
|
|
28
|
+
try {
|
|
29
|
+
return useUnifiedAuth();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
log.debug('useSuperAdminBypass', 'UnifiedAuthProvider not available, falling back to defaults', {
|
|
32
|
+
error: error instanceof Error ? error.message : error
|
|
33
|
+
});
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useSuperAdminBypass(): SuperAdminBypassState {
|
|
39
|
+
const authContext = useSafeUnifiedAuth();
|
|
40
|
+
const user = authContext?.user ?? null;
|
|
41
|
+
const supabase = authContext?.supabase ?? null;
|
|
42
|
+
const metadataHint =
|
|
43
|
+
Boolean(user?.app_metadata?.is_super_admin) ||
|
|
44
|
+
Boolean(user?.user_metadata?.is_super_admin);
|
|
45
|
+
|
|
46
|
+
const [isSuperAdminState, setIsSuperAdminState] = useState<boolean>(metadataHint);
|
|
47
|
+
const [hasVerified, setHasVerified] = useState<boolean>(!user?.id);
|
|
48
|
+
const [isLoading, setIsLoading] = useState<boolean>(!!user?.id);
|
|
49
|
+
const [error, setError] = useState<Error | null>(null);
|
|
50
|
+
const lastOverrideValueRef = useRef<boolean | null>(null);
|
|
51
|
+
|
|
52
|
+
// Verify against the RBAC engine whenever the user changes
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!user?.id) {
|
|
55
|
+
setIsSuperAdminState(false);
|
|
56
|
+
setHasVerified(true);
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
setError(null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let cancelled = false;
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
setError(null);
|
|
65
|
+
|
|
66
|
+
fetchIsSuperAdmin(user.id)
|
|
67
|
+
.then((result) => {
|
|
68
|
+
if (cancelled) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
setIsSuperAdminState(result);
|
|
72
|
+
setHasVerified(true);
|
|
73
|
+
})
|
|
74
|
+
.catch((err) => {
|
|
75
|
+
if (cancelled) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const normalisedError =
|
|
79
|
+
err instanceof Error ? err : new Error('Failed to resolve super admin status');
|
|
80
|
+
setError(normalisedError);
|
|
81
|
+
setIsSuperAdminState(false);
|
|
82
|
+
setHasVerified(false);
|
|
83
|
+
log.error('Unable to verify super admin status', normalisedError);
|
|
84
|
+
})
|
|
85
|
+
.finally(() => {
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
cancelled = true;
|
|
93
|
+
};
|
|
94
|
+
}, [user?.id]);
|
|
95
|
+
|
|
96
|
+
const shouldBypass = hasVerified && isSuperAdminState;
|
|
97
|
+
|
|
98
|
+
// Keep the database session flag in sync for auditing/RLS helpers
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!supabase) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (lastOverrideValueRef.current === shouldBypass) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
lastOverrideValueRef.current = shouldBypass;
|
|
107
|
+
|
|
108
|
+
setSuperAdminOverrideFlag({
|
|
109
|
+
supabase,
|
|
110
|
+
enabled: shouldBypass,
|
|
111
|
+
reason: 'pace-core-super-admin-bypass'
|
|
112
|
+
}).catch(() => {
|
|
113
|
+
// Errors are logged inside the helper
|
|
114
|
+
});
|
|
115
|
+
}, [supabase, shouldBypass]);
|
|
116
|
+
|
|
117
|
+
return useMemo(
|
|
118
|
+
() => ({
|
|
119
|
+
isSuperAdmin: shouldBypass,
|
|
120
|
+
isLoading,
|
|
121
|
+
error
|
|
122
|
+
}),
|
|
123
|
+
[shouldBypass, isLoading, error]
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
@@ -27,7 +27,7 @@ const inFlightRequests = new Map<string, Promise<boolean>>();
|
|
|
27
27
|
function generateDeduplicationKey(input: PermissionCheck): string {
|
|
28
28
|
return RBACCache.generatePermissionKey({
|
|
29
29
|
userId: input.userId,
|
|
30
|
-
organisationId: input.scope.organisationId
|
|
30
|
+
organisationId: input.scope.organisationId, // Can be undefined for page-level permissions
|
|
31
31
|
eventId: input.scope.eventId,
|
|
32
32
|
appId: input.scope.appId,
|
|
33
33
|
permission: input.permission,
|