@jmruthers/pace-core 0.5.188 → 0.5.190
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-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
- package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-C4uxosp6.d.ts} +129 -40
- package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
- package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
- package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
- package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
- package/dist/chunk-4QYC5L4K.js.map +1 -0
- package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
- package/dist/chunk-73HSNNOQ.js.map +1 -0
- package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
- package/dist/chunk-DZWK57KZ.js.map +1 -0
- package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
- package/dist/chunk-HQVPB5MZ.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-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
- package/dist/chunk-J2XXC7R5.js.map +1 -0
- package/dist/{chunk-UNOTYLQF.js → chunk-NIU6J6OX.js} +772 -725
- package/dist/chunk-NIU6J6OX.js.map +1 -0
- package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
- package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
- package/dist/chunk-RUYZKXOD.js.map +1 -0
- package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
- package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
- package/dist/{chunk-IPCH26AG.js → chunk-STYK4OH2.js} +11 -11
- package/dist/chunk-STYK4OH2.js.map +1 -0
- package/dist/{chunk-EFCLXK7F.js → chunk-VVBAW5A5.js} +4201 -3809
- package/dist/chunk-VVBAW5A5.js.map +1 -0
- package/dist/chunk-Y4BUBBHD.js +614 -0
- package/dist/chunk-Y4BUBBHD.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 +3 -5
- package/dist/components.js +19 -23
- package/dist/components.js.map +1 -1
- 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 +10 -5
- package/dist/hooks.js +14 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +13 -12
- package/dist/index.js +79 -73
- 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 +76 -12
- package/dist/rbac/index.js +12 -9
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
- 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 +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +4 -4
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +2 -2
- package/docs/api/classes/SecureSupabaseClient.md +21 -16
- 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 +128 -0
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +4 -4
- 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 +155 -135
- package/docs/api-reference/components.md +72 -29
- 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/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 -4
- 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 +9 -9
- package/src/__tests__/rls-policies.test.ts +197 -61
- package/src/components/AddressField/AddressField.test.tsx +42 -0
- package/src/components/AddressField/AddressField.tsx +71 -60
- package/src/components/AddressField/README.md +1 -0
- package/src/components/Alert/Alert.test.tsx +50 -10
- package/src/components/Alert/Alert.tsx +5 -3
- package/src/components/Avatar/Avatar.test.tsx +252 -226
- package/src/components/Avatar/Avatar.tsx +179 -53
- package/src/components/Avatar/index.ts +1 -1
- 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 +8 -8
- 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.tsx +5 -5
- 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.test.tsx +7 -9
- package/src/components/UserMenu/UserMenu.tsx +10 -8
- package/src/components/index.ts +2 -1
- 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.unit.test.ts → usePublicEvent.test.ts} +28 -1
- package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEvent.ts +2 -2
- package/src/hooks/public/usePublicFileDisplay.ts +173 -87
- package/src/hooks/useAppConfig.ts +24 -5
- package/src/hooks/useFileDisplay.ts +297 -34
- 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 +21 -0
- package/src/hooks/useSecureDataAccess.test.ts +80 -35
- package/src/hooks/useSecureDataAccess.ts +80 -37
- package/src/index.ts +2 -1
- 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 +6 -1
- package/src/rbac/utils/contextValidator.ts +288 -0
- package/src/rbac/utils/eventContext.ts +48 -2
- package/src/services/EventService.ts +165 -21
- package/src/services/OrganisationService.ts +37 -2
- package/src/services/__tests__/EventService.test.ts +26 -21
- package/src/types/file-reference.ts +13 -10
- package/src/utils/app/appNameResolver.test.ts +346 -73
- package/src/utils/context/superAdminOverride.ts +58 -0
- package/src/utils/file-reference/index.ts +61 -33
- package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
- 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-EFCLXK7F.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-IPCH26AG.js.map +0 -1
- package/dist/chunk-SAUPYVLF.js.map +0 -1
- package/dist/chunk-THRPYOFK.js.map +0 -1
- package/dist/chunk-UNOTYLQF.js.map +0 -1
- package/dist/chunk-VGZZXKBR.js.map +0 -1
- package/dist/chunk-YHCN776L.js.map +0 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
- package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
- /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
- /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
- /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
|
@@ -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,284 @@ 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('organisation_memberships')
|
|
251
|
+
.select('organisation_id')
|
|
252
|
+
.eq('user_id', user.id)
|
|
253
|
+
.or('status.is.null,status.eq.active');
|
|
254
|
+
|
|
255
|
+
if (membershipError) {
|
|
256
|
+
logger.warn('useFileDisplay', 'Error querying organisation memberships:', membershipError);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (memberships && memberships.length > 0) {
|
|
260
|
+
// Try each organisation the user belongs to
|
|
261
|
+
const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
|
|
262
|
+
|
|
263
|
+
// Query each organisation in parallel
|
|
264
|
+
const orgQueries = orgIds.map(async (orgId) => {
|
|
265
|
+
try {
|
|
266
|
+
if (category) {
|
|
267
|
+
return await service.getFilesByCategory(
|
|
268
|
+
table_name,
|
|
269
|
+
record_id,
|
|
270
|
+
category,
|
|
271
|
+
orgId
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
return await service.listFileReferences(
|
|
275
|
+
table_name,
|
|
276
|
+
record_id,
|
|
277
|
+
orgId
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
// Silently fail for individual org queries - user might not have access
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const orgResults = await Promise.all(orgQueries);
|
|
287
|
+
orgScopedFiles = orgResults.flat();
|
|
288
|
+
} else {
|
|
289
|
+
// When user has no organisation memberships, try querying files with any organisation_id
|
|
290
|
+
// as a fallback - the file might be organisation-scoped but accessible via RLS
|
|
291
|
+
// This handles edge cases where RLS allows access but we can't enumerate organisations
|
|
292
|
+
try {
|
|
293
|
+
// Try querying without organisation_id filter - let RLS handle security
|
|
294
|
+
let fallbackQuery = supabase
|
|
295
|
+
.from('file_references')
|
|
296
|
+
.select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
|
|
297
|
+
.eq('table_name', table_name)
|
|
298
|
+
.eq('record_id', record_id)
|
|
299
|
+
.order('created_at', { ascending: false });
|
|
300
|
+
|
|
301
|
+
if (category) {
|
|
302
|
+
fallbackQuery = fallbackQuery.eq('file_metadata->>category', category);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const { data: fallbackFiles } = await fallbackQuery;
|
|
306
|
+
|
|
307
|
+
if (fallbackFiles && fallbackFiles.length > 0) {
|
|
308
|
+
// Convert to FileReference format
|
|
309
|
+
orgScopedFiles = fallbackFiles.map((f: any) => ({
|
|
310
|
+
id: f.id,
|
|
311
|
+
table_name: f.table_name,
|
|
312
|
+
record_id: f.record_id,
|
|
313
|
+
file_path: f.file_path,
|
|
314
|
+
file_metadata: f.file_metadata || {},
|
|
315
|
+
organisation_id: f.organisation_id,
|
|
316
|
+
app_id: f.app_id,
|
|
317
|
+
is_public: f.is_public ?? false,
|
|
318
|
+
created_at: f.created_at,
|
|
319
|
+
updated_at: f.updated_at
|
|
320
|
+
})) as FileReference[];
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
// Silently fail - RLS may block or other error
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
logger.warn('useFileDisplay', 'Error querying organisation-scoped files:', err);
|
|
329
|
+
orgScopedFiles = [];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Merge results: prefer organisation-scoped files if both exist, otherwise use user-scoped
|
|
333
|
+
// Sort by created_at DESC to get most recent first
|
|
334
|
+
const allFiles = [...userScopedFiles, ...orgScopedFiles];
|
|
335
|
+
allFiles.sort((a, b) => {
|
|
336
|
+
const aTime = new Date(a.created_at).getTime();
|
|
337
|
+
const bTime = new Date(b.created_at).getTime();
|
|
338
|
+
return bTime - aTime;
|
|
214
339
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
organisation_id
|
|
220
|
-
|
|
340
|
+
|
|
341
|
+
// If we have both types, prefer organisation-scoped (non-null organisation_id)
|
|
342
|
+
// Otherwise, use whatever we found
|
|
343
|
+
if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
|
|
344
|
+
// Prefer organisation-scoped files - filter to only those with organisation_id
|
|
345
|
+
files = allFiles.filter(f => f.organisation_id !== null);
|
|
346
|
+
} else {
|
|
347
|
+
// Use all files found (either user-scoped or org-scoped, but not both)
|
|
348
|
+
files = allFiles;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// If no files found through RPC, try a direct query as fallback
|
|
352
|
+
// This handles cases where RLS policy allows access but RPC security check is too strict
|
|
353
|
+
// (e.g., pace_person files where user owns the person record but record_id != user_id)
|
|
354
|
+
if (files.length === 0) {
|
|
355
|
+
try {
|
|
356
|
+
let directQuery = supabase
|
|
357
|
+
.from('file_references')
|
|
358
|
+
.select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
|
|
359
|
+
.eq('table_name', table_name)
|
|
360
|
+
.eq('record_id', record_id)
|
|
361
|
+
.order('created_at', { ascending: false });
|
|
362
|
+
|
|
363
|
+
// If category is provided, filter by category in metadata
|
|
364
|
+
if (category) {
|
|
365
|
+
directQuery = directQuery.eq('file_metadata->>category', category);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { data: directFiles } = await directQuery;
|
|
369
|
+
|
|
370
|
+
if (directFiles && directFiles.length > 0) {
|
|
371
|
+
// Convert to FileReference format
|
|
372
|
+
files = directFiles.map((f: any) => ({
|
|
373
|
+
id: f.id,
|
|
374
|
+
table_name: f.table_name,
|
|
375
|
+
record_id: f.record_id,
|
|
376
|
+
file_path: f.file_path,
|
|
377
|
+
file_metadata: f.file_metadata || {},
|
|
378
|
+
organisation_id: f.organisation_id,
|
|
379
|
+
app_id: f.app_id,
|
|
380
|
+
is_public: f.is_public ?? false,
|
|
381
|
+
created_at: f.created_at,
|
|
382
|
+
updated_at: f.updated_at
|
|
383
|
+
})) as FileReference[];
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
// Silently fail - RLS may block or other error
|
|
387
|
+
}
|
|
388
|
+
}
|
|
221
389
|
} else {
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
390
|
+
// organisation_id is provided (or explicitly null) - use normal query
|
|
391
|
+
// CRITICAL: When category is provided, MUST use RPC function, not direct queries
|
|
392
|
+
// Category is stored in file_metadata JSONB field, not a direct column
|
|
393
|
+
if (category) {
|
|
394
|
+
// Single file mode - get files by category using RPC
|
|
395
|
+
logger.debug('useFileDisplay', 'Using RPC function for category filtering:', {
|
|
396
|
+
table_name,
|
|
397
|
+
record_id,
|
|
398
|
+
category,
|
|
399
|
+
organisation_id
|
|
400
|
+
});
|
|
401
|
+
files = await service.getFilesByCategory(
|
|
402
|
+
table_name,
|
|
403
|
+
record_id,
|
|
404
|
+
category,
|
|
405
|
+
organisation_id
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
// Multiple files mode - get all files using RPC
|
|
409
|
+
files = await service.listFileReferences(
|
|
410
|
+
table_name,
|
|
411
|
+
record_id,
|
|
412
|
+
organisation_id
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Fallback: If no files found and organisation_id is falsy (undefined, null, empty string),
|
|
417
|
+
// try searching both scopes as a fallback
|
|
418
|
+
// This handles cases where the prop might be passed as empty string or the check above didn't catch it
|
|
419
|
+
if (files.length === 0 && (!organisation_id || organisation_id === '')) {
|
|
420
|
+
// Try the dual-scope search logic
|
|
421
|
+
let userScopedFiles: FileReference[] = [];
|
|
422
|
+
let orgScopedFiles: FileReference[] = [];
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
if (category) {
|
|
426
|
+
userScopedFiles = await service.getFilesByCategory(
|
|
427
|
+
table_name,
|
|
428
|
+
record_id,
|
|
429
|
+
category,
|
|
430
|
+
undefined
|
|
431
|
+
);
|
|
432
|
+
} else {
|
|
433
|
+
userScopedFiles = await service.listFileReferences(
|
|
434
|
+
table_name,
|
|
435
|
+
record_id,
|
|
436
|
+
undefined
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
// Silently fail
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
445
|
+
if (user) {
|
|
446
|
+
const { data: memberships } = await supabase
|
|
447
|
+
.from('organisation_memberships')
|
|
448
|
+
.select('organisation_id')
|
|
449
|
+
.eq('user_id', user.id)
|
|
450
|
+
.or('status.is.null,status.eq.active');
|
|
451
|
+
|
|
452
|
+
if (memberships && memberships.length > 0) {
|
|
453
|
+
const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
|
|
454
|
+
const orgQueries = orgIds.map(async (orgId) => {
|
|
455
|
+
try {
|
|
456
|
+
if (category) {
|
|
457
|
+
return await service.getFilesByCategory(table_name, record_id, category, orgId);
|
|
458
|
+
} else {
|
|
459
|
+
return await service.listFileReferences(table_name, record_id, orgId);
|
|
460
|
+
}
|
|
461
|
+
} catch (err) {
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
const orgResults = await Promise.all(orgQueries);
|
|
466
|
+
orgScopedFiles = orgResults.flat();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
// Silently fail
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Merge results
|
|
474
|
+
const allFiles = [...userScopedFiles, ...orgScopedFiles];
|
|
475
|
+
allFiles.sort((a, b) => {
|
|
476
|
+
const aTime = new Date(a.created_at).getTime();
|
|
477
|
+
const bTime = new Date(b.created_at).getTime();
|
|
478
|
+
return bTime - aTime;
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
|
|
482
|
+
files = allFiles.filter(f => f.organisation_id !== null);
|
|
483
|
+
} else {
|
|
484
|
+
files = allFiles;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
228
487
|
}
|
|
229
488
|
|
|
230
489
|
if (files.length === 0) {
|
|
@@ -271,6 +530,7 @@ export function useFileDisplay(
|
|
|
271
530
|
const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
|
|
272
531
|
appName: 'pace-core',
|
|
273
532
|
orgId: organisation_id,
|
|
533
|
+
userId: organisation_id ? undefined : record_id,
|
|
274
534
|
expiresIn: 3600
|
|
275
535
|
});
|
|
276
536
|
url = signedUrlResult?.url || null;
|
|
@@ -283,6 +543,7 @@ export function useFileDisplay(
|
|
|
283
543
|
const urlMap = await generateFileUrlsBatch(supabase, files, {
|
|
284
544
|
appName: 'pace-core',
|
|
285
545
|
orgId: organisation_id,
|
|
546
|
+
userId: organisation_id ? undefined : record_id,
|
|
286
547
|
expiresIn: 3600
|
|
287
548
|
});
|
|
288
549
|
setFileUrls(urlMap);
|
|
@@ -344,7 +605,7 @@ export function useFileDisplay(
|
|
|
344
605
|
|
|
345
606
|
// Fetch files when parameters change
|
|
346
607
|
useEffect(() => {
|
|
347
|
-
if (table_name && record_id &&
|
|
608
|
+
if (table_name && record_id && supabase) {
|
|
348
609
|
fetchFiles();
|
|
349
610
|
} else {
|
|
350
611
|
setFileUrl(null);
|
|
@@ -355,14 +616,16 @@ export function useFileDisplay(
|
|
|
355
616
|
setIsLoading(false);
|
|
356
617
|
setError(null);
|
|
357
618
|
}
|
|
358
|
-
|
|
619
|
+
// fetchFiles is memoized; we only need to re-run when parameters change
|
|
620
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
621
|
+
}, [table_name, record_id, organisation_id, supabase]);
|
|
359
622
|
|
|
360
623
|
const refetch = useCallback(async (): Promise<void> => {
|
|
361
|
-
if (!table_name || !record_id || !
|
|
624
|
+
if (!table_name || !record_id || !supabase) return;
|
|
362
625
|
|
|
363
626
|
// Clear cache for this file
|
|
364
627
|
if (enableCache) {
|
|
365
|
-
const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
|
|
628
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
|
|
366
629
|
authenticatedFileCache.delete(cacheKey);
|
|
367
630
|
}
|
|
368
631
|
await fetchFiles();
|
|
@@ -415,14 +678,14 @@ export function getFileDisplayCacheStats(): { size: number; keys: string[] } {
|
|
|
415
678
|
export function invalidateFileDisplayCache(
|
|
416
679
|
table_name: string,
|
|
417
680
|
record_id: string,
|
|
418
|
-
organisation_id: string,
|
|
681
|
+
organisation_id: string | null | undefined,
|
|
419
682
|
category?: FileCategory
|
|
420
683
|
): void {
|
|
421
|
-
const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
|
|
684
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
|
|
422
685
|
authenticatedFileCache.delete(cacheKey);
|
|
423
686
|
// Also invalidate 'all' category if specific category invalidated
|
|
424
687
|
if (category) {
|
|
425
|
-
const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id}_all`;
|
|
688
|
+
const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_all`;
|
|
426
689
|
authenticatedFileCache.delete(allCategoryKey);
|
|
427
690
|
}
|
|
428
691
|
}
|
|
@@ -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(() => {
|