@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module Hooks
|
|
5
5
|
*
|
|
6
6
|
* A React hook for accessing file references in authenticated contexts.
|
|
7
|
-
* Can handle both public and private files using the
|
|
7
|
+
* Can handle both public and private files using the core_file_references system.
|
|
8
8
|
*
|
|
9
9
|
* Features:
|
|
10
10
|
* - Works in authenticated contexts
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* function FileView() {
|
|
22
22
|
* const { fileUrl, fileReference, isLoading, error } = useFileDisplay(
|
|
23
|
-
* '
|
|
23
|
+
* 'core_events',
|
|
24
24
|
* eventId,
|
|
25
25
|
* organisationId,
|
|
26
26
|
* FileCategory.EVENT_LOGOS
|
|
@@ -135,7 +135,7 @@ export function useFileDisplay(
|
|
|
135
135
|
const [error, setError] = useState<Error | null>(null);
|
|
136
136
|
|
|
137
137
|
const fetchFiles = useCallback(async (): Promise<void> => {
|
|
138
|
-
if (!table_name || !record_id || !
|
|
138
|
+
if (!table_name || !record_id || !supabase) {
|
|
139
139
|
setFileUrl(null);
|
|
140
140
|
setFileReference(null);
|
|
141
141
|
setFileReferences([]);
|
|
@@ -145,14 +145,17 @@ export function useFileDisplay(
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// Validate UUID format for organisationId to prevent database errors
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
// Validate UUID format for organisationId to prevent database errors (only if provided)
|
|
149
|
+
if (organisation_id) {
|
|
150
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
151
|
+
if (!uuidRegex.test(organisation_id)) {
|
|
152
|
+
logger.warn('useFileDisplay', 'Invalid organisationId format (not a valid UUID):', organisation_id);
|
|
153
|
+
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
// Check cache first
|
|
155
|
-
|
|
157
|
+
// When organisation_id is undefined, use 'undefined' in cache key to distinguish from explicit null
|
|
158
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
|
|
156
159
|
if (enableCache) {
|
|
157
160
|
const cached = authenticatedFileCache.get(cacheKey);
|
|
158
161
|
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
@@ -166,6 +169,7 @@ export function useFileDisplay(
|
|
|
166
169
|
const signedUrlResult = await getSignedUrl(supabase, cachedData.fileReference.file_path, {
|
|
167
170
|
appName: 'pace-core',
|
|
168
171
|
orgId: organisation_id,
|
|
172
|
+
userId: organisation_id ? undefined : record_id,
|
|
169
173
|
expiresIn: 3600
|
|
170
174
|
});
|
|
171
175
|
const regeneratedUrl = signedUrlResult?.url || null;
|
|
@@ -202,29 +206,283 @@ export function useFileDisplay(
|
|
|
202
206
|
const service = createFileReferenceService(supabase);
|
|
203
207
|
let files: FileReference[] = [];
|
|
204
208
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
// When organisation_id is undefined (not provided), search both user-scoped (null) and organisation-scoped files
|
|
210
|
+
// This allows FileDisplay to work without requiring the organisation_id prop
|
|
211
|
+
// Note: Explicitly passing null or empty string should only search user-scoped files
|
|
212
|
+
const shouldSearchBothScopes = organisation_id === undefined;
|
|
213
|
+
|
|
214
|
+
if (shouldSearchBothScopes) {
|
|
215
|
+
// First, try user-scoped files (organisation_id = null)
|
|
216
|
+
let userScopedFiles: FileReference[] = [];
|
|
217
|
+
let orgScopedFiles: FileReference[] = [];
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
if (category) {
|
|
221
|
+
userScopedFiles = await service.getFilesByCategory(
|
|
222
|
+
table_name,
|
|
223
|
+
record_id,
|
|
224
|
+
category,
|
|
225
|
+
undefined // Explicitly pass undefined for user-scoped files (service converts to null for RPC)
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
userScopedFiles = await service.listFileReferences(
|
|
229
|
+
table_name,
|
|
230
|
+
record_id,
|
|
231
|
+
undefined // Explicitly pass undefined for user-scoped files (service converts to null for RPC)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
logger.warn('useFileDisplay', 'Error querying user-scoped files:', err);
|
|
236
|
+
userScopedFiles = [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// For organisation-scoped files, we need to query the user's organisations
|
|
240
|
+
// Get user's organisations from the authenticated session
|
|
241
|
+
try {
|
|
242
|
+
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
|
243
|
+
if (userError) {
|
|
244
|
+
logger.warn('useFileDisplay', 'Error getting user:', userError);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (user) {
|
|
248
|
+
// Query user's active organisation memberships
|
|
249
|
+
const { data: memberships, error: membershipError } = await supabase
|
|
250
|
+
.from('core_organisation_memberships')
|
|
251
|
+
.select('organisation_id')
|
|
252
|
+
.eq('user_id', user.id);
|
|
253
|
+
|
|
254
|
+
if (membershipError) {
|
|
255
|
+
logger.warn('useFileDisplay', 'Error querying organisation memberships:', membershipError);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (memberships && memberships.length > 0) {
|
|
259
|
+
// Try each organisation the user belongs to
|
|
260
|
+
const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
|
|
261
|
+
|
|
262
|
+
// Query each organisation in parallel
|
|
263
|
+
const orgQueries = orgIds.map(async (orgId) => {
|
|
264
|
+
try {
|
|
265
|
+
if (category) {
|
|
266
|
+
return await service.getFilesByCategory(
|
|
267
|
+
table_name,
|
|
268
|
+
record_id,
|
|
269
|
+
category,
|
|
270
|
+
orgId
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
return await service.listFileReferences(
|
|
274
|
+
table_name,
|
|
275
|
+
record_id,
|
|
276
|
+
orgId
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
// Silently fail for individual org queries - user might not have access
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const orgResults = await Promise.all(orgQueries);
|
|
286
|
+
orgScopedFiles = orgResults.flat();
|
|
287
|
+
} else {
|
|
288
|
+
// When user has no organisation memberships, try querying files with any organisation_id
|
|
289
|
+
// as a fallback - the file might be organisation-scoped but accessible via RLS
|
|
290
|
+
// This handles edge cases where RLS allows access but we can't enumerate organisations
|
|
291
|
+
try {
|
|
292
|
+
// Try querying without organisation_id filter - let RLS handle security
|
|
293
|
+
let fallbackQuery = supabase
|
|
294
|
+
.from('core_file_references')
|
|
295
|
+
.select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
|
|
296
|
+
.eq('table_name', table_name)
|
|
297
|
+
.eq('record_id', record_id)
|
|
298
|
+
.order('created_at', { ascending: false });
|
|
299
|
+
|
|
300
|
+
if (category) {
|
|
301
|
+
fallbackQuery = fallbackQuery.eq('file_metadata->>category', category);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { data: fallbackFiles } = await fallbackQuery;
|
|
305
|
+
|
|
306
|
+
if (fallbackFiles && fallbackFiles.length > 0) {
|
|
307
|
+
// Convert to FileReference format
|
|
308
|
+
orgScopedFiles = fallbackFiles.map((f: any) => ({
|
|
309
|
+
id: f.id,
|
|
310
|
+
table_name: f.table_name,
|
|
311
|
+
record_id: f.record_id,
|
|
312
|
+
file_path: f.file_path,
|
|
313
|
+
file_metadata: f.file_metadata || {},
|
|
314
|
+
organisation_id: f.organisation_id,
|
|
315
|
+
app_id: f.app_id,
|
|
316
|
+
is_public: f.is_public ?? false,
|
|
317
|
+
created_at: f.created_at,
|
|
318
|
+
updated_at: f.updated_at
|
|
319
|
+
})) as FileReference[];
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
// Silently fail - RLS may block or other error
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
logger.warn('useFileDisplay', 'Error querying organisation-scoped files:', err);
|
|
328
|
+
orgScopedFiles = [];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Merge results: prefer organisation-scoped files if both exist, otherwise use user-scoped
|
|
332
|
+
// Sort by created_at DESC to get most recent first
|
|
333
|
+
const allFiles = [...userScopedFiles, ...orgScopedFiles];
|
|
334
|
+
allFiles.sort((a, b) => {
|
|
335
|
+
const aTime = new Date(a.created_at).getTime();
|
|
336
|
+
const bTime = new Date(b.created_at).getTime();
|
|
337
|
+
return bTime - aTime;
|
|
214
338
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
organisation_id
|
|
220
|
-
|
|
339
|
+
|
|
340
|
+
// If we have both types, prefer organisation-scoped (non-null organisation_id)
|
|
341
|
+
// Otherwise, use whatever we found
|
|
342
|
+
if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
|
|
343
|
+
// Prefer organisation-scoped files - filter to only those with organisation_id
|
|
344
|
+
files = allFiles.filter(f => f.organisation_id !== null);
|
|
345
|
+
} else {
|
|
346
|
+
// Use all files found (either user-scoped or org-scoped, but not both)
|
|
347
|
+
files = allFiles;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// If no files found through RPC, try a direct query as fallback
|
|
351
|
+
// This handles cases where RLS policy allows access but RPC security check is too strict
|
|
352
|
+
// (e.g., core_person files where user owns the person record but record_id != user_id)
|
|
353
|
+
if (files.length === 0) {
|
|
354
|
+
try {
|
|
355
|
+
let directQuery = supabase
|
|
356
|
+
.from('core_file_references')
|
|
357
|
+
.select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
|
|
358
|
+
.eq('table_name', table_name)
|
|
359
|
+
.eq('record_id', record_id)
|
|
360
|
+
.order('created_at', { ascending: false });
|
|
361
|
+
|
|
362
|
+
// If category is provided, filter by category in metadata
|
|
363
|
+
if (category) {
|
|
364
|
+
directQuery = directQuery.eq('file_metadata->>category', category);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const { data: directFiles } = await directQuery;
|
|
368
|
+
|
|
369
|
+
if (directFiles && directFiles.length > 0) {
|
|
370
|
+
// Convert to FileReference format
|
|
371
|
+
files = directFiles.map((f: any) => ({
|
|
372
|
+
id: f.id,
|
|
373
|
+
table_name: f.table_name,
|
|
374
|
+
record_id: f.record_id,
|
|
375
|
+
file_path: f.file_path,
|
|
376
|
+
file_metadata: f.file_metadata || {},
|
|
377
|
+
organisation_id: f.organisation_id,
|
|
378
|
+
app_id: f.app_id,
|
|
379
|
+
is_public: f.is_public ?? false,
|
|
380
|
+
created_at: f.created_at,
|
|
381
|
+
updated_at: f.updated_at
|
|
382
|
+
})) as FileReference[];
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
// Silently fail - RLS may block or other error
|
|
386
|
+
}
|
|
387
|
+
}
|
|
221
388
|
} else {
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
389
|
+
// organisation_id is provided (or explicitly null) - use normal query
|
|
390
|
+
// CRITICAL: When category is provided, MUST use RPC function, not direct queries
|
|
391
|
+
// Category is stored in file_metadata JSONB field, not a direct column
|
|
392
|
+
if (category) {
|
|
393
|
+
// Single file mode - get files by category using RPC
|
|
394
|
+
logger.debug('useFileDisplay', 'Using RPC function for category filtering:', {
|
|
395
|
+
table_name,
|
|
396
|
+
record_id,
|
|
397
|
+
category,
|
|
398
|
+
organisation_id
|
|
399
|
+
});
|
|
400
|
+
files = await service.getFilesByCategory(
|
|
401
|
+
table_name,
|
|
402
|
+
record_id,
|
|
403
|
+
category,
|
|
404
|
+
organisation_id
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
// Multiple files mode - get all files using RPC
|
|
408
|
+
files = await service.listFileReferences(
|
|
409
|
+
table_name,
|
|
410
|
+
record_id,
|
|
411
|
+
organisation_id
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Fallback: If no files found and organisation_id is falsy (undefined, null, empty string),
|
|
416
|
+
// try searching both scopes as a fallback
|
|
417
|
+
// This handles cases where the prop might be passed as empty string or the check above didn't catch it
|
|
418
|
+
if (files.length === 0 && (!organisation_id || organisation_id === '')) {
|
|
419
|
+
// Try the dual-scope search logic
|
|
420
|
+
let userScopedFiles: FileReference[] = [];
|
|
421
|
+
let orgScopedFiles: FileReference[] = [];
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
if (category) {
|
|
425
|
+
userScopedFiles = await service.getFilesByCategory(
|
|
426
|
+
table_name,
|
|
427
|
+
record_id,
|
|
428
|
+
category,
|
|
429
|
+
undefined
|
|
430
|
+
);
|
|
431
|
+
} else {
|
|
432
|
+
userScopedFiles = await service.listFileReferences(
|
|
433
|
+
table_name,
|
|
434
|
+
record_id,
|
|
435
|
+
undefined
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
// Silently fail
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
444
|
+
if (user) {
|
|
445
|
+
const { data: memberships } = await supabase
|
|
446
|
+
.from('core_organisation_memberships')
|
|
447
|
+
.select('organisation_id')
|
|
448
|
+
.eq('user_id', user.id)
|
|
449
|
+
.or('status.is.null,status.eq.active');
|
|
450
|
+
|
|
451
|
+
if (memberships && memberships.length > 0) {
|
|
452
|
+
const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
|
|
453
|
+
const orgQueries = orgIds.map(async (orgId) => {
|
|
454
|
+
try {
|
|
455
|
+
if (category) {
|
|
456
|
+
return await service.getFilesByCategory(table_name, record_id, category, orgId);
|
|
457
|
+
} else {
|
|
458
|
+
return await service.listFileReferences(table_name, record_id, orgId);
|
|
459
|
+
}
|
|
460
|
+
} catch (err) {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
const orgResults = await Promise.all(orgQueries);
|
|
465
|
+
orgScopedFiles = orgResults.flat();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} catch (err) {
|
|
469
|
+
// Silently fail
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Merge results
|
|
473
|
+
const allFiles = [...userScopedFiles, ...orgScopedFiles];
|
|
474
|
+
allFiles.sort((a, b) => {
|
|
475
|
+
const aTime = new Date(a.created_at).getTime();
|
|
476
|
+
const bTime = new Date(b.created_at).getTime();
|
|
477
|
+
return bTime - aTime;
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
|
|
481
|
+
files = allFiles.filter(f => f.organisation_id !== null);
|
|
482
|
+
} else {
|
|
483
|
+
files = allFiles;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
228
486
|
}
|
|
229
487
|
|
|
230
488
|
if (files.length === 0) {
|
|
@@ -271,6 +529,7 @@ export function useFileDisplay(
|
|
|
271
529
|
const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
|
|
272
530
|
appName: 'pace-core',
|
|
273
531
|
orgId: organisation_id,
|
|
532
|
+
userId: organisation_id ? undefined : record_id,
|
|
274
533
|
expiresIn: 3600
|
|
275
534
|
});
|
|
276
535
|
url = signedUrlResult?.url || null;
|
|
@@ -283,6 +542,7 @@ export function useFileDisplay(
|
|
|
283
542
|
const urlMap = await generateFileUrlsBatch(supabase, files, {
|
|
284
543
|
appName: 'pace-core',
|
|
285
544
|
orgId: organisation_id,
|
|
545
|
+
userId: organisation_id ? undefined : record_id,
|
|
286
546
|
expiresIn: 3600
|
|
287
547
|
});
|
|
288
548
|
setFileUrls(urlMap);
|
|
@@ -344,7 +604,7 @@ export function useFileDisplay(
|
|
|
344
604
|
|
|
345
605
|
// Fetch files when parameters change
|
|
346
606
|
useEffect(() => {
|
|
347
|
-
if (table_name && record_id &&
|
|
607
|
+
if (table_name && record_id && supabase) {
|
|
348
608
|
fetchFiles();
|
|
349
609
|
} else {
|
|
350
610
|
setFileUrl(null);
|
|
@@ -355,14 +615,16 @@ export function useFileDisplay(
|
|
|
355
615
|
setIsLoading(false);
|
|
356
616
|
setError(null);
|
|
357
617
|
}
|
|
358
|
-
|
|
618
|
+
// fetchFiles is memoized; we only need to re-run when parameters change
|
|
619
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
620
|
+
}, [table_name, record_id, organisation_id, supabase]);
|
|
359
621
|
|
|
360
622
|
const refetch = useCallback(async (): Promise<void> => {
|
|
361
|
-
if (!table_name || !record_id || !
|
|
623
|
+
if (!table_name || !record_id || !supabase) return;
|
|
362
624
|
|
|
363
625
|
// Clear cache for this file
|
|
364
626
|
if (enableCache) {
|
|
365
|
-
const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
|
|
627
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
|
|
366
628
|
authenticatedFileCache.delete(cacheKey);
|
|
367
629
|
}
|
|
368
630
|
await fetchFiles();
|
|
@@ -415,14 +677,14 @@ export function getFileDisplayCacheStats(): { size: number; keys: string[] } {
|
|
|
415
677
|
export function invalidateFileDisplayCache(
|
|
416
678
|
table_name: string,
|
|
417
679
|
record_id: string,
|
|
418
|
-
organisation_id: string,
|
|
680
|
+
organisation_id: string | null | undefined,
|
|
419
681
|
category?: FileCategory
|
|
420
682
|
): void {
|
|
421
|
-
const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
|
|
683
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
|
|
422
684
|
authenticatedFileCache.delete(cacheKey);
|
|
423
685
|
// Also invalidate 'all' category if specific category invalidated
|
|
424
686
|
if (category) {
|
|
425
|
-
const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id}_all`;
|
|
687
|
+
const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_all`;
|
|
426
688
|
authenticatedFileCache.delete(allCategoryKey);
|
|
427
689
|
}
|
|
428
690
|
}
|
|
@@ -16,6 +16,49 @@ import { createLogger } from '../utils/core/logger';
|
|
|
16
16
|
|
|
17
17
|
const log = createLogger('useFileReference');
|
|
18
18
|
|
|
19
|
+
type UrlRefreshCallback = () => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Shared interval manager to avoid spawning multiple intervals for the same file reference
|
|
23
|
+
*/
|
|
24
|
+
const urlRefreshManager = {
|
|
25
|
+
subscriptions: new Map<string, { callbacks: Set<UrlRefreshCallback>; intervalId: NodeJS.Timeout | null }>(),
|
|
26
|
+
|
|
27
|
+
subscribe(key: string, callback: UrlRefreshCallback) {
|
|
28
|
+
let entry = this.subscriptions.get(key);
|
|
29
|
+
if (!entry) {
|
|
30
|
+
entry = { callbacks: new Set(), intervalId: null };
|
|
31
|
+
this.subscriptions.set(key, entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
entry.callbacks.add(callback);
|
|
35
|
+
|
|
36
|
+
if (!entry.intervalId) {
|
|
37
|
+
entry.intervalId = setInterval(() => {
|
|
38
|
+
entry?.callbacks.forEach((cb) => cb());
|
|
39
|
+
}, 55 * 60 * 1000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
this.unsubscribe(key, callback);
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
unsubscribe(key: string, callback: UrlRefreshCallback) {
|
|
48
|
+
const entry = this.subscriptions.get(key);
|
|
49
|
+
if (!entry) return;
|
|
50
|
+
|
|
51
|
+
entry.callbacks.delete(callback);
|
|
52
|
+
|
|
53
|
+
if (entry.callbacks.size === 0) {
|
|
54
|
+
if (entry.intervalId) {
|
|
55
|
+
clearInterval(entry.intervalId);
|
|
56
|
+
}
|
|
57
|
+
this.subscriptions.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
19
62
|
export function useFileReference(supabase: SupabaseClient) {
|
|
20
63
|
const [isLoading, setIsLoading] = useState(false);
|
|
21
64
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -221,7 +264,7 @@ export function useFileReferenceForRecord(
|
|
|
221
264
|
const [fileReference, setFileReference] = useState<FileReference | null>(null);
|
|
222
265
|
const [fileReferences, setFileReferences] = useState<FileReference[]>([]);
|
|
223
266
|
const [fileCount, setFileCount] = useState<number>(0);
|
|
224
|
-
const
|
|
267
|
+
const refreshSubscriptionRef = useRef<(() => void) | null>(null);
|
|
225
268
|
|
|
226
269
|
const loadFileReference = useCallback(async () => {
|
|
227
270
|
const reference = await getFileReference(table_name, record_id, organisation_id);
|
|
@@ -259,27 +302,29 @@ export function useFileReferenceForRecord(
|
|
|
259
302
|
|
|
260
303
|
// Auto-refresh signed URLs before expiration (refresh 5 minutes before 1 hour expiry)
|
|
261
304
|
useEffect(() => {
|
|
305
|
+
if (refreshSubscriptionRef.current) {
|
|
306
|
+
refreshSubscriptionRef.current();
|
|
307
|
+
refreshSubscriptionRef.current = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
262
310
|
if (!fileReference || fileReference.is_public) {
|
|
263
311
|
// Only refresh private files (signed URLs expire)
|
|
264
|
-
if (urlRefreshIntervalRef.current) {
|
|
265
|
-
clearInterval(urlRefreshIntervalRef.current);
|
|
266
|
-
urlRefreshIntervalRef.current = null;
|
|
267
|
-
}
|
|
268
312
|
return;
|
|
269
313
|
}
|
|
270
314
|
|
|
271
315
|
// Refresh signed URL 5 minutes before expiration (55 minutes into the 1-hour expiry)
|
|
272
|
-
|
|
316
|
+
const key = `${fileReference.table_name}:${fileReference.record_id}:${organisation_id}`;
|
|
317
|
+
refreshSubscriptionRef.current = urlRefreshManager.subscribe(key, () => {
|
|
273
318
|
loadFileUrl();
|
|
274
|
-
}
|
|
319
|
+
});
|
|
275
320
|
|
|
276
321
|
return () => {
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
322
|
+
if (refreshSubscriptionRef.current) {
|
|
323
|
+
refreshSubscriptionRef.current();
|
|
324
|
+
refreshSubscriptionRef.current = null;
|
|
280
325
|
}
|
|
281
326
|
};
|
|
282
|
-
}, [fileReference, loadFileUrl]);
|
|
327
|
+
}, [fileReference, loadFileUrl, organisation_id]);
|
|
283
328
|
|
|
284
329
|
return {
|
|
285
330
|
isLoading,
|
package/src/hooks/useFileUrl.ts
CHANGED
|
@@ -17,7 +17,7 @@ const log = createLogger('useFileUrl');
|
|
|
17
17
|
|
|
18
18
|
export interface UseFileUrlOptions {
|
|
19
19
|
/** Organisation ID for signed URL generation */
|
|
20
|
-
organisation_id: string;
|
|
20
|
+
organisation_id: string | undefined;
|
|
21
21
|
/** Supabase client instance */
|
|
22
22
|
supabase: SupabaseClient;
|
|
23
23
|
/** Whether to auto-load URLs on mount */
|
|
@@ -156,6 +156,15 @@ export function useInactivityTracker({
|
|
|
156
156
|
const lastActivityRef = useRef<number>(Date.now());
|
|
157
157
|
const channelRef = useRef<BroadcastChannel | null>(null);
|
|
158
158
|
const throttledResetActivityRef = useRef<((event: Event) => void) | null>(null);
|
|
159
|
+
const onIdleRef = useRef(onIdle);
|
|
160
|
+
const onWarningRef = useRef(onWarning);
|
|
161
|
+
const onActivityRef = useRef(onActivity);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
onIdleRef.current = onIdle;
|
|
165
|
+
onWarningRef.current = onWarning;
|
|
166
|
+
onActivityRef.current = onActivity;
|
|
167
|
+
}, [onIdle, onWarning, onActivity]);
|
|
159
168
|
|
|
160
169
|
// Clear all timers
|
|
161
170
|
const clearTimers = useCallback(() => {
|
|
@@ -190,7 +199,7 @@ export function useInactivityTracker({
|
|
|
190
199
|
|
|
191
200
|
// Notify activity callback (unless skipped for initial setup)
|
|
192
201
|
if (!skipActivityCallback) {
|
|
193
|
-
|
|
202
|
+
onActivityRef.current?.();
|
|
194
203
|
}
|
|
195
204
|
|
|
196
205
|
// Set up warning timer
|
|
@@ -198,14 +207,14 @@ export function useInactivityTracker({
|
|
|
198
207
|
if (warningTime > 0) {
|
|
199
208
|
warningTimeoutRef.current = setTimeout(() => {
|
|
200
209
|
setShowWarning(true);
|
|
201
|
-
|
|
210
|
+
onWarningRef.current?.();
|
|
202
211
|
}, warningTime);
|
|
203
212
|
}
|
|
204
213
|
|
|
205
214
|
// Set up idle timeout
|
|
206
215
|
timeoutRef.current = setTimeout(() => {
|
|
207
216
|
setIsIdle(true);
|
|
208
|
-
|
|
217
|
+
onIdleRef.current?.();
|
|
209
218
|
}, idleTimeoutMs);
|
|
210
219
|
|
|
211
220
|
// Start countdown interval for time remaining
|
|
@@ -234,7 +243,7 @@ export function useInactivityTracker({
|
|
|
234
243
|
} catch (error) {
|
|
235
244
|
logger.warn('useInactivityTracker', 'Failed to broadcast activity:', error);
|
|
236
245
|
}
|
|
237
|
-
}, [enabled, idleTimeoutMs, warnBeforeMs,
|
|
246
|
+
}, [enabled, idleTimeoutMs, warnBeforeMs, storageKey, clearTimers]);
|
|
238
247
|
|
|
239
248
|
// Start tracking
|
|
240
249
|
const startTracking = useCallback(() => {
|
|
@@ -281,12 +290,12 @@ export function useInactivityTracker({
|
|
|
281
290
|
|
|
282
291
|
if (remaining <= warnBeforeMs) {
|
|
283
292
|
setShowWarning(true);
|
|
284
|
-
|
|
293
|
+
onWarningRef.current?.();
|
|
285
294
|
}
|
|
286
295
|
|
|
287
296
|
if (remaining <= 0) {
|
|
288
297
|
setIsIdle(true);
|
|
289
|
-
|
|
298
|
+
onIdleRef.current?.();
|
|
290
299
|
return;
|
|
291
300
|
}
|
|
292
301
|
}
|
|
@@ -330,7 +339,7 @@ export function useInactivityTracker({
|
|
|
330
339
|
channelRef.current = null;
|
|
331
340
|
}
|
|
332
341
|
};
|
|
333
|
-
}, [enabled,
|
|
342
|
+
}, [enabled, channelName, storageKey, idleTimeoutMs, warnBeforeMs, resetActivity, clearTimers]);
|
|
334
343
|
|
|
335
344
|
// Stop tracking
|
|
336
345
|
const stopTracking = useCallback(() => {
|