@jmruthers/pace-core 0.5.193 → 0.6.2
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/CHANGELOG.md +62 -0
- package/README.md +7 -1
- package/cursor-rules/00-pace-core-compliance.mdc +299 -0
- package/cursor-rules/01-standards-compliance.mdc +244 -0
- package/cursor-rules/02-project-structure.mdc +200 -0
- package/cursor-rules/03-solid-principles.mdc +222 -0
- package/cursor-rules/04-testing-standards.mdc +268 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +246 -0
- package/cursor-rules/06-code-quality.mdc +309 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +214 -0
- package/cursor-rules/08-markup-quality.mdc +452 -0
- package/cursor-rules/CHANGELOG.md +119 -0
- package/cursor-rules/README.md +192 -0
- package/dist/{AuthService-DjnJHDtC.d.ts → AuthService-BPvc3Ka0.d.ts} +54 -0
- package/dist/{DataTable-Be6dH_dR.d.ts → DataTable-BMRU8a1j.d.ts} +34 -2
- package/dist/{DataTable-5FU7IESH.js → DataTable-TPTKCX4D.js} +10 -9
- package/dist/{PublicPageProvider-C0Sm_e5k.d.ts → PublicPageProvider-DC6kCaqf.d.ts} +385 -261
- package/dist/{UnifiedAuthProvider-RGJTDE2C.js → UnifiedAuthProvider-CH6Z342H.js} +3 -3
- package/dist/{UnifiedAuthProvider-185Ih4dj.d.ts → UnifiedAuthProvider-CVcTjx-d.d.ts} +29 -0
- package/dist/{api-N774RPUA.js → api-MVVQZLJI.js} +2 -2
- package/dist/{chunk-KNC55RTG.js → chunk-24UVZUZG.js} +90 -54
- package/dist/chunk-24UVZUZG.js.map +1 -0
- package/dist/{chunk-HWIIPPNI.js → chunk-2UOI2FG5.js} +20 -20
- package/dist/chunk-2UOI2FG5.js.map +1 -0
- package/dist/{chunk-E3SPN4VZ 5.js → chunk-3XC4CPTD.js} +4345 -3986
- package/dist/chunk-3XC4CPTD.js.map +1 -0
- package/dist/{chunk-7EQTDTTJ.js → chunk-6J4GEEJR.js} +172 -45
- package/dist/chunk-6J4GEEJR.js.map +1 -0
- package/dist/{chunk-6C4YBBJM 5.js → chunk-6SOIHG6Z.js} +1 -1
- package/dist/chunk-6SOIHG6Z.js.map +1 -0
- package/dist/{chunk-7FLMSG37.js → chunk-EHMR7VYL.js} +25 -25
- package/dist/chunk-EHMR7VYL.js.map +1 -0
- package/dist/{chunk-I7PSE6JW.js → chunk-F2IMUDXZ.js} +2 -75
- package/dist/chunk-F2IMUDXZ.js.map +1 -0
- package/dist/{chunk-QWWZ5CAQ.js → chunk-FFQEQTNW.js} +7 -9
- package/dist/chunk-FFQEQTNW.js.map +1 -0
- package/dist/chunk-FMUCXFII.js +76 -0
- package/dist/chunk-FMUCXFII.js.map +1 -0
- package/dist/{chunk-HW3OVDUF.js → chunk-J36DSWQK.js} +1 -1
- package/dist/{chunk-HW3OVDUF.js.map → chunk-J36DSWQK.js.map} +1 -1
- package/dist/{chunk-SQGMNID3.js → chunk-L4OXEN46.js} +4 -5
- package/dist/chunk-L4OXEN46.js.map +1 -0
- package/dist/{chunk-R77UEZ4E 3.js → chunk-M43Y4SSO.js} +1 -1
- package/dist/chunk-M43Y4SSO.js.map +1 -0
- package/dist/{chunk-IIELH4DL.js → chunk-MMZ7JXPU.js} +60 -223
- package/dist/chunk-MMZ7JXPU.js.map +1 -0
- package/dist/{chunk-NOAYCWCX 5.js → chunk-NECFR5MM.js} +394 -312
- package/dist/chunk-NECFR5MM.js.map +1 -0
- package/dist/{chunk-BC4IJKSL.js → chunk-SFZUDBL5.js} +40 -4
- package/dist/chunk-SFZUDBL5.js.map +1 -0
- package/dist/{chunk-XNXXZ43G.js → chunk-XWQCNGTQ.js} +748 -364
- package/dist/chunk-XWQCNGTQ.js.map +1 -0
- package/dist/components.d.ts +6 -6
- package/dist/components.js +15 -12
- package/dist/components.js.map +1 -1
- package/dist/{functions-D_kgHktt.d.ts → functions-DHebl8-F.d.ts} +1 -1
- package/dist/hooks.d.ts +59 -126
- package/dist/hooks.js +19 -28
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +63 -16
- package/dist/index.js +23 -24
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +21 -3
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +146 -115
- package/dist/rbac/index.js +8 -11
- package/dist/theming/runtime.d.ts +1 -13
- package/dist/theming/runtime.js +1 -1
- package/dist/{timezone-_pgH8qrY.d.ts → timezone-CHhWg6b4.d.ts} +3 -10
- package/dist/{types-UU913iLA.d.ts → types-BeoeWV5I.d.ts} +8 -0
- package/dist/{types-CEpcvwwF.d.ts → types-CkbwOr4Y.d.ts} +6 -0
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-TZe0gy-4.d.ts → usePublicRouteParams-1oMokgLF.d.ts} +34 -4
- package/dist/{useToast-C8gR5ir4.d.ts → useToast-AyaT-x7p.d.ts} +2 -2
- package/dist/utils.d.ts +4 -5
- package/dist/utils.js +15 -15
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +7 -1
- package/docs/api/classes/ColumnFactory.md +8 -8
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +18 -15
- package/docs/api/classes/StorageUtils.md +1 -1
- 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 +4 -4
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +9 -2
- package/docs/api/interfaces/ButtonProps.md +7 -4
- package/docs/api/interfaces/CalendarProps.md +8 -5
- package/docs/api/interfaces/CardProps.md +8 -5
- 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 +24 -21
- package/docs/api/interfaces/DataTableColumn.md +31 -31
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +5 -5
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/ErrorBoundaryProps.md +147 -0
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +36 -0
- package/docs/api/interfaces/ErrorBoundaryState.md +75 -0
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +8 -8
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +26 -23
- package/docs/api/interfaces/FooterProps.md +10 -8
- package/docs/api/interfaces/FormFieldProps.md +10 -10
- 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 +7 -4
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +14 -11
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +11 -11
- package/docs/api/interfaces/NavigationMenuProps.md +15 -15
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +30 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +6 -4
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +7 -26
- package/docs/api/interfaces/PublicPageFooterProps.md +9 -9
- package/docs/api/interfaces/PublicPageHeaderProps.md +10 -10
- package/docs/api/interfaces/PublicPageLayoutProps.md +7 -20
- 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 +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- 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 +3 -3
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/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 +3 -3
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +4 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +58 -55
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +15 -13
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +11 -9
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +8 -8
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +9 -6
- package/docs/api/interfaces/UsePublicEventOptions.md +3 -3
- package/docs/api/interfaces/UsePublicEventReturn.md +8 -5
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +4 -4
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +12 -9
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +10 -7
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +14 -11
- package/docs/api/interfaces/UserMenuProps.md +8 -6
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +575 -634
- package/docs/architecture/database-schema-requirements.md +161 -0
- package/docs/core-concepts/rbac-system.md +3 -3
- package/docs/documentation-index.md +2 -4
- package/docs/getting-started/cursor-rules.md +263 -0
- package/docs/getting-started/installation-guide.md +6 -1
- package/docs/getting-started/quick-start.md +6 -1
- package/docs/migration/DOCUMENTATION_STRUCTURE.md +441 -0
- package/docs/migration/MIGRATION_GUIDE.md +6 -28
- package/docs/migration/README.md +52 -6
- package/docs/migration/V0.5.190_TO_V0.6.1_MIGRATION.md +1153 -0
- package/docs/migration/V0.6.0_REACT_19_MIGRATION.md +227 -0
- package/docs/migration/database-changes-december-2025.md +3 -3
- package/docs/rbac/event-based-apps.md +1 -1
- package/docs/rbac/getting-started.md +1 -1
- package/docs/rbac/quick-start.md +1 -1
- package/docs/standards/README.md +40 -0
- package/docs/troubleshooting/migration.md +4 -4
- package/examples/PublicPages/PublicEventPage.tsx +1 -1
- package/package.json +12 -6
- package/scripts/audit/core/checks/accessibility.cjs +197 -0
- package/scripts/audit/core/checks/api-usage.cjs +191 -0
- package/scripts/audit/core/checks/bundle.cjs +142 -0
- package/scripts/{check-pace-core-compliance.cjs → audit/core/checks/compliance.cjs} +737 -691
- package/scripts/audit/core/checks/config.cjs +54 -0
- package/scripts/audit/core/checks/coverage.cjs +84 -0
- package/scripts/audit/core/checks/dependencies.cjs +454 -0
- package/scripts/audit/core/checks/documentation.cjs +203 -0
- package/scripts/audit/core/checks/environment.cjs +128 -0
- package/scripts/audit/core/checks/error-handling.cjs +299 -0
- package/scripts/audit/core/checks/forms.cjs +172 -0
- package/scripts/audit/core/checks/heuristics.cjs +68 -0
- package/scripts/audit/core/checks/hooks.cjs +334 -0
- package/scripts/audit/core/checks/imports.cjs +244 -0
- package/scripts/audit/core/checks/performance.cjs +325 -0
- package/scripts/audit/core/checks/routes.cjs +117 -0
- package/scripts/audit/core/checks/state.cjs +130 -0
- package/scripts/audit/core/checks/structure.cjs +65 -0
- package/scripts/audit/core/checks/style.cjs +584 -0
- package/scripts/audit/core/checks/testing.cjs +122 -0
- package/scripts/audit/core/checks/typescript.cjs +61 -0
- package/scripts/audit/core/scanner.cjs +199 -0
- package/scripts/audit/core/utils.cjs +137 -0
- package/scripts/audit/index.cjs +223 -0
- package/scripts/audit/reporters/console.cjs +151 -0
- package/scripts/audit/reporters/json.cjs +54 -0
- package/scripts/audit/reporters/markdown.cjs +124 -0
- package/scripts/audit-consuming-app.cjs +86 -0
- package/scripts/build-docs/build-decision.js +240 -0
- package/scripts/build-docs/cache-utils.js +105 -0
- package/scripts/build-docs/content-normalization.js +150 -0
- package/scripts/build-docs/file-utils.js +105 -0
- package/scripts/build-docs/git-utils.js +86 -0
- package/scripts/build-docs/hash-utils.js +116 -0
- package/scripts/build-docs/typedoc-runner.js +220 -0
- package/scripts/build-docs-incremental.js +77 -913
- package/scripts/install-cursor-rules.cjs +236 -0
- package/scripts/utils/command-runner.js +16 -11
- package/scripts/validate-formats.js +61 -56
- package/scripts/validate-master.js +74 -69
- package/scripts/validate-pre-publish.js +70 -65
- package/src/__tests__/helpers/test-providers.tsx +1 -1
- package/src/__tests__/helpers/test-utils.tsx +1 -1
- package/src/__tests__/hooks/usePermissions.test.ts +2 -2
- package/src/components/Alert/Alert.test.tsx +12 -18
- package/src/components/Alert/Alert.tsx +5 -7
- package/src/components/Avatar/Avatar.test.tsx +4 -4
- package/src/components/Badge/Badge.tsx +16 -4
- package/src/components/Button/Button.tsx +27 -4
- package/src/components/Calendar/Calendar.tsx +9 -3
- package/src/components/Card/Card.tsx +4 -0
- package/src/components/Checkbox/Checkbox.test.tsx +12 -12
- package/src/components/Checkbox/Checkbox.tsx +2 -2
- package/src/components/DataTable/DataTable.test.tsx +57 -93
- package/src/components/DataTable/DataTable.tsx +40 -6
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +5 -6
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +29 -7
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +12 -12
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +2 -3
- package/src/components/DataTable/components/AccessDeniedPage.tsx +17 -26
- package/src/components/DataTable/components/ActionButtons.tsx +10 -7
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
- package/src/components/DataTable/components/ColumnFilter.tsx +10 -0
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +12 -0
- package/src/components/DataTable/components/DataTableBody.tsx +8 -0
- package/src/components/DataTable/components/DataTableCore.tsx +200 -561
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +11 -0
- package/src/components/DataTable/components/DataTableLayout.tsx +559 -0
- package/src/components/DataTable/components/DataTableModals.tsx +9 -1
- package/src/components/DataTable/components/DataTableToolbar.tsx +8 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +12 -0
- package/src/components/DataTable/components/EditFields.tsx +307 -0
- package/src/components/DataTable/components/EditableRow.tsx +9 -1
- package/src/components/DataTable/components/EmptyState.tsx +10 -0
- package/src/components/DataTable/components/FilterRow.tsx +12 -0
- package/src/components/DataTable/components/GroupHeader.tsx +12 -0
- package/src/components/DataTable/components/GroupingDropdown.tsx +12 -0
- package/src/components/DataTable/components/ImportModal.tsx +7 -0
- package/src/components/DataTable/components/LoadingState.tsx +6 -0
- package/src/components/DataTable/components/PaginationControls.tsx +16 -1
- package/src/components/DataTable/components/RowComponent.tsx +391 -0
- package/src/components/DataTable/components/UnifiedTableBody.tsx +62 -852
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +16 -4
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +4 -2
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +23 -23
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +11 -11
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +36 -36
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +27 -27
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +39 -39
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +33 -33
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +29 -29
- package/src/components/DataTable/components/cellValueUtils.ts +40 -0
- package/src/components/DataTable/components/hooks/useImportModalFocus.ts +53 -0
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +126 -0
- package/src/components/DataTable/context/DataTableContext.tsx +50 -0
- package/src/components/DataTable/core/ColumnFactory.ts +31 -0
- package/src/components/DataTable/core/DataTableContext.tsx +32 -1
- package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +10 -0
- package/src/components/DataTable/hooks/useColumnReordering.ts +14 -2
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +10 -0
- package/src/components/DataTable/hooks/useDataTableDataPipeline.ts +16 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +124 -32
- package/src/components/DataTable/hooks/useDataTableState.ts +35 -1
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +12 -0
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +2 -2
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +11 -0
- package/src/components/DataTable/hooks/useTableColumns.ts +8 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +14 -0
- package/src/components/DataTable/styles.ts +6 -6
- package/src/components/DataTable/types.ts +6 -10
- package/src/components/DataTable/utils/a11yUtils.ts +7 -0
- package/src/components/DataTable/utils/debugTools.ts +18 -113
- package/src/components/DataTable/utils/errorHandling.ts +12 -0
- package/src/components/DataTable/utils/exportUtils.ts +9 -0
- package/src/components/DataTable/utils/flexibleImport.ts +12 -48
- package/src/components/DataTable/utils/paginationUtils.ts +8 -0
- package/src/components/DataTable/utils/performanceUtils.ts +5 -1
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -14
- package/src/components/Dialog/Dialog.tsx +8 -7
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +180 -1
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +46 -6
- package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +129 -0
- package/src/components/ErrorBoundary/index.ts +27 -2
- package/src/components/EventSelector/EventSelector.tsx +4 -1
- package/src/components/FileDisplay/FileDisplay.test.tsx +2 -2
- package/src/components/FileDisplay/FileDisplay.tsx +32 -18
- package/src/components/FileUpload/FileUpload.tsx +22 -2
- package/src/components/Footer/Footer.test.tsx +16 -16
- package/src/components/Footer/Footer.tsx +15 -12
- package/src/components/Form/Form.test.tsx +36 -15
- package/src/components/Form/Form.tsx +31 -26
- package/src/components/Header/Header.tsx +22 -11
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +40 -40
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +1 -1
- package/src/components/Input/Input.test.tsx +2 -2
- package/src/components/Input/Input.tsx +36 -34
- package/src/components/Label/Label.tsx +1 -1
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +4 -4
- package/src/components/LoadingSpinner/LoadingSpinner.tsx +1 -1
- package/src/components/LoginForm/LoginForm.test.tsx +42 -42
- package/src/components/LoginForm/LoginForm.tsx +12 -8
- package/src/components/NavigationMenu/NavigationMenu.tsx +15 -514
- package/src/components/NavigationMenu/types.ts +56 -0
- package/src/components/NavigationMenu/useNavigationFiltering.ts +390 -0
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +3 -0
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +1 -1
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +54 -52
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +33 -12
- package/src/components/PaceAppLayout/README.md +1 -1
- package/src/components/PaceAppLayout/test-setup.tsx +1 -2
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +4 -1
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +33 -33
- package/src/components/PasswordChange/PasswordChangeForm.tsx +10 -1
- package/src/components/Progress/Progress.tsx +1 -1
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +3 -9
- package/src/components/PublicLayout/PublicPageLayout.tsx +3 -6
- package/src/components/PublicLayout/PublicPageProvider.tsx +4 -0
- package/src/components/Select/Select.tsx +95 -438
- package/src/components/Select/context.ts +23 -0
- package/src/components/Select/hooks/useSelectEvents.ts +87 -0
- package/src/components/Select/hooks/useSelectSearch.ts +91 -0
- package/src/components/Select/hooks/useSelectState.ts +104 -0
- package/src/components/Select/index.ts +9 -1
- package/src/components/Select/types.ts +123 -0
- package/src/components/Select/utils/text.ts +26 -0
- package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +5 -6
- package/src/components/Switch/Switch.tsx +4 -4
- package/src/components/Table/Table.tsx +1 -1
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Textarea/Textarea.tsx +27 -29
- package/src/components/Toast/Toast.tsx +5 -1
- package/src/components/Tooltip/Tooltip.tsx +3 -3
- package/src/components/UserMenu/UserMenu.test.tsx +24 -11
- package/src/components/UserMenu/UserMenu.tsx +22 -19
- package/src/components/index.ts +2 -2
- package/src/hooks/__tests__/hooks.integration.test.tsx +80 -55
- package/src/hooks/__tests__/index.unit.test.ts +2 -5
- package/src/hooks/__tests__/useStorage.unit.test.ts +36 -36
- package/src/hooks/index.ts +1 -2
- package/src/hooks/public/usePublicEvent.ts +5 -1
- package/src/hooks/public/usePublicEventLogo.ts +5 -1
- package/src/hooks/public/usePublicFileDisplay.ts +4 -0
- package/src/hooks/public/usePublicRouteParams.ts +5 -1
- package/src/hooks/services/useAuth.ts +32 -0
- package/src/hooks/services/useCurrentEvent.ts +6 -0
- package/src/hooks/services/useCurrentOrganisation.ts +6 -0
- package/src/hooks/useDataTableState.ts +8 -18
- package/src/hooks/useDebounce.ts +9 -0
- package/src/hooks/useEventTheme.ts +6 -0
- package/src/hooks/useFileDisplay.ts +4 -0
- package/src/hooks/useFileReference.ts +25 -7
- package/src/hooks/useFileUrl.ts +11 -1
- package/src/hooks/useFocusManagement.ts +16 -2
- package/src/hooks/useFocusTrap.ts +7 -4
- package/src/hooks/useFormDialog.ts +8 -7
- package/src/hooks/useInactivityTracker.ts +4 -1
- package/src/hooks/useKeyboardShortcuts.ts +4 -0
- package/src/hooks/useOrganisationPermissions.ts +4 -0
- package/src/hooks/useOrganisationSecurity.ts +4 -0
- package/src/hooks/usePerformanceMonitor.ts +4 -0
- package/src/hooks/usePermissionCache.ts +8 -1
- package/src/hooks/useQueryCache.ts +12 -1
- package/src/hooks/useSessionRestoration.ts +4 -0
- package/src/hooks/useStorage.ts +4 -0
- package/src/hooks/useToast.ts +3 -3
- package/src/index.ts +2 -1
- package/src/providers/__tests__/OrganisationProvider.test.tsx +115 -49
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +21 -6
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +10 -10
- package/src/providers/services/AuthServiceProvider.tsx +18 -0
- package/src/providers/services/EventServiceProvider.tsx +18 -0
- package/src/providers/services/InactivityServiceProvider.tsx +18 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +18 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +58 -22
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +33 -7
- package/src/rbac/README.md +1 -1
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +26 -26
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +4 -5
- package/src/rbac/adapters.tsx +14 -5
- package/src/rbac/api.ts +100 -67
- package/src/rbac/components/EnhancedNavigationMenu.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +1 -1
- package/src/rbac/components/NavigationProvider.tsx +5 -2
- package/src/rbac/components/PagePermissionGuard.tsx +158 -18
- package/src/rbac/components/PagePermissionProvider.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +1 -1
- package/src/rbac/components/RoleBasedRouter.tsx +6 -2
- package/src/rbac/components/SecureDataProvider.test.tsx +84 -49
- package/src/rbac/components/SecureDataProvider.tsx +21 -6
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +24 -14
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +7 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +14 -6
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +15 -4
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +148 -24
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +81 -15
- package/src/rbac/engine.ts +38 -14
- package/src/rbac/hooks/permissions/index.ts +7 -0
- package/src/rbac/hooks/permissions/useAccessLevel.ts +105 -0
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +79 -0
- package/src/rbac/hooks/permissions/useCan.ts +347 -0
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +90 -0
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +90 -0
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +93 -0
- package/src/rbac/hooks/permissions/usePermissions.ts +253 -0
- package/src/rbac/hooks/useCan.test.ts +71 -64
- package/src/rbac/hooks/usePermissions.ts +14 -995
- package/src/rbac/hooks/useResourcePermissions.test.ts +54 -18
- package/src/rbac/hooks/useResourcePermissions.ts +14 -4
- package/src/rbac/hooks/useSecureSupabase.ts +33 -13
- package/src/rbac/permissions.ts +0 -30
- package/src/rbac/secureClient.ts +212 -61
- package/src/rbac/types.ts +8 -0
- package/src/theming/__tests__/parseEventColours.test.ts +6 -9
- package/src/theming/parseEventColours.ts +5 -19
- package/src/types/vitest-globals.d.ts +51 -26
- package/src/utils/__mocks__/supabaseMock.ts +1 -3
- package/src/utils/__tests__/formatting.unit.test.ts +4 -4
- package/src/utils/__tests__/index.unit.test.ts +2 -2
- package/src/utils/audit/audit.ts +0 -3
- package/src/utils/core/cn.ts +1 -1
- package/src/utils/file-reference/index.ts +53 -1
- package/src/utils/formatting/formatting.ts +8 -18
- package/src/utils/index.ts +0 -1
- package/src/utils/security/secureDataAccess.test.ts +31 -20
- package/src/utils/security/secureDataAccess.ts +4 -3
- package/dist/chunk-6C4YBBJM.js +0 -628
- package/dist/chunk-6C4YBBJM.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js 2.map +0 -1
- package/dist/chunk-7EQTDTTJ.js 2.map +0 -1
- package/dist/chunk-7EQTDTTJ.js.map +0 -1
- package/dist/chunk-7FLMSG37.js 2.map +0 -1
- package/dist/chunk-7FLMSG37.js.map +0 -1
- package/dist/chunk-BC4IJKSL.js.map +0 -1
- package/dist/chunk-E3SPN4VZ.js +0 -12917
- package/dist/chunk-E3SPN4VZ.js.map +0 -1
- package/dist/chunk-E66EQZE6 5.js +0 -37
- package/dist/chunk-E66EQZE6.js 2.map +0 -1
- package/dist/chunk-HWIIPPNI.js.map +0 -1
- package/dist/chunk-I7PSE6JW 5.js +0 -191
- package/dist/chunk-I7PSE6JW.js 2.map +0 -1
- package/dist/chunk-I7PSE6JW.js.map +0 -1
- package/dist/chunk-IIELH4DL.js.map +0 -1
- package/dist/chunk-KNC55RTG.js 5.map +0 -1
- package/dist/chunk-KNC55RTG.js.map +0 -1
- package/dist/chunk-KQCRWDSA.js 5.map +0 -1
- package/dist/chunk-LFNCN2SP.js +0 -412
- package/dist/chunk-LFNCN2SP.js 2.map +0 -1
- package/dist/chunk-LFNCN2SP.js.map +0 -1
- package/dist/chunk-LMC26NLJ 2.js +0 -84
- package/dist/chunk-NOAYCWCX.js +0 -4993
- package/dist/chunk-NOAYCWCX.js.map +0 -1
- package/dist/chunk-QWWZ5CAQ.js 3.map +0 -1
- package/dist/chunk-QWWZ5CAQ.js.map +0 -1
- package/dist/chunk-QXHPKYJV 3.js +0 -113
- package/dist/chunk-R77UEZ4E.js +0 -68
- package/dist/chunk-R77UEZ4E.js.map +0 -1
- package/dist/chunk-SQGMNID3.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js 6.map +0 -1
- package/dist/chunk-XNXXZ43G.js.map +0 -1
- package/dist/chunk-ZSAAAMVR 6.js +0 -25
- package/dist/components.js 5.map +0 -1
- package/dist/styles/index 2.js +0 -12
- package/dist/styles/index.js 5.map +0 -1
- package/dist/theming/runtime 5.js +0 -19
- package/dist/theming/runtime.js 5.map +0 -1
- package/docs/api/classes/ErrorBoundary.md +0 -144
- package/docs/migration/quick-migration-guide.md +0 -356
- package/docs/migration/service-architecture.md +0 -281
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +0 -680
- package/src/hooks/useSecureDataAccess.test.ts +0 -559
- package/src/hooks/useSecureDataAccess.ts +0 -666
- /package/dist/{DataTable-5FU7IESH.js.map → DataTable-TPTKCX4D.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-RGJTDE2C.js.map → UnifiedAuthProvider-CH6Z342H.js.map} +0 -0
- /package/dist/{api-N774RPUA.js.map → api-MVVQZLJI.js.map} +0 -0
- /package/docs/migration/{organisation-context-timing-fix.md → V0.3.44_organisation-context-timing-fix.md} +0 -0
- /package/docs/migration/{rbac-migration.md → V0.4.0_rbac-migration.md} +0 -0
- /package/docs/migration/{person-scoped-profiles-migration-guide.md → V0.5.190_person-scoped-profiles-migration-guide.md} +0 -0
- /package/examples/{rbac → RBAC}/CompleteRBACExample.tsx +0 -0
- /package/examples/{rbac → RBAC}/EventBasedApp.tsx +0 -0
- /package/examples/{rbac → RBAC}/PermissionExample.tsx +0 -0
- /package/examples/{rbac → RBAC}/index.ts +0 -0
package/src/rbac/engine.ts
CHANGED
|
@@ -535,22 +535,46 @@ export class RBACEngine {
|
|
|
535
535
|
return cached;
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
const startTime = Date.now();
|
|
538
539
|
const now = new Date().toISOString();
|
|
539
|
-
const { data, error } = await this.supabase
|
|
540
|
-
.from('rbac_global_roles')
|
|
541
|
-
.select('role')
|
|
542
|
-
.eq('user_id', userId)
|
|
543
|
-
.eq('role', 'super_admin')
|
|
544
|
-
.lte('valid_from', now)
|
|
545
|
-
.or(`valid_to.is.null,valid_to.gte.${now}`)
|
|
546
|
-
.limit(1) as { data: Array<{ role: string }> | null; error: any };
|
|
547
|
-
|
|
548
|
-
const isSuperAdmin = !error && data && data.length > 0;
|
|
549
|
-
|
|
550
|
-
// Cache for 60 seconds
|
|
551
|
-
rbacCache.set(cacheKey, isSuperAdmin, 60000);
|
|
552
540
|
|
|
553
|
-
|
|
541
|
+
try {
|
|
542
|
+
const { data, error } = await this.supabase
|
|
543
|
+
.from('rbac_global_roles')
|
|
544
|
+
.select('role')
|
|
545
|
+
.eq('user_id', userId)
|
|
546
|
+
.eq('role', 'super_admin')
|
|
547
|
+
.lte('valid_from', now)
|
|
548
|
+
.or(`valid_to.is.null,valid_to.gte.${now}`)
|
|
549
|
+
.limit(1) as { data: Array<{ role: string }> | null; error: any };
|
|
550
|
+
|
|
551
|
+
const elapsed = Date.now() - startTime;
|
|
552
|
+
|
|
553
|
+
// Log warning if query takes too long
|
|
554
|
+
if (elapsed > 2000) {
|
|
555
|
+
console.warn('[RBACEngine] Super admin check took longer than expected', {
|
|
556
|
+
userId,
|
|
557
|
+
elapsedMs: elapsed,
|
|
558
|
+
error: error?.message,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const isSuperAdmin = !error && data && data.length > 0;
|
|
563
|
+
|
|
564
|
+
// Cache for 60 seconds
|
|
565
|
+
rbacCache.set(cacheKey, isSuperAdmin, 60000);
|
|
566
|
+
|
|
567
|
+
return Boolean(isSuperAdmin);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
const elapsed = Date.now() - startTime;
|
|
570
|
+
console.error('[RBACEngine] Error checking super admin', {
|
|
571
|
+
userId,
|
|
572
|
+
error: err,
|
|
573
|
+
elapsedMs: elapsed,
|
|
574
|
+
});
|
|
575
|
+
// Return false on error (fail secure)
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
554
578
|
}
|
|
555
579
|
|
|
556
580
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { useAccessLevel } from './useAccessLevel';
|
|
2
|
+
export { useCachedPermissions } from './useCachedPermissions';
|
|
3
|
+
export { useCan } from './useCan';
|
|
4
|
+
export { useHasAllPermissions } from './useHasAllPermissions';
|
|
5
|
+
export { useHasAnyPermission } from './useHasAnyPermission';
|
|
6
|
+
export { useMultiplePermissions } from './useMultiplePermissions';
|
|
7
|
+
export { usePermissions } from './usePermissions';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { getAccessLevel } from '../../api';
|
|
4
|
+
import { OrganisationContextRequiredError } from '../../types';
|
|
5
|
+
import type { AccessLevel as AccessLevelType, Scope, UUID } from '../../types';
|
|
6
|
+
import { useAppConfig } from '../../../hooks/useAppConfig';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to get user's access level in a scope
|
|
10
|
+
*
|
|
11
|
+
* @param userId - User ID
|
|
12
|
+
* @param scope - Scope for access level checking
|
|
13
|
+
* @returns Access level state and methods
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function MyComponent() {
|
|
18
|
+
* const { accessLevel, isLoading, error } = useAccessLevel(userId, scope);
|
|
19
|
+
*
|
|
20
|
+
* if (isLoading) return <div>Loading access level...</div>;
|
|
21
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
22
|
+
*
|
|
23
|
+
* return (
|
|
24
|
+
* <div>
|
|
25
|
+
* Access Level: {accessLevel}
|
|
26
|
+
* {accessLevel >= AccessLevel.ADMIN && <AdminPanel />}
|
|
27
|
+
* </div>
|
|
28
|
+
* );
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function useAccessLevel(userId: UUID, scope: Scope): {
|
|
33
|
+
accessLevel: AccessLevelType;
|
|
34
|
+
isLoading: boolean;
|
|
35
|
+
error: Error | null;
|
|
36
|
+
refetch: () => Promise<void>;
|
|
37
|
+
} {
|
|
38
|
+
const [accessLevel, setAccessLevel] = useState<AccessLevelType>('viewer');
|
|
39
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
40
|
+
const [error, setError] = useState<Error | null>(null);
|
|
41
|
+
|
|
42
|
+
// Get appName from context if available (safely handles missing context)
|
|
43
|
+
let appName: string | undefined;
|
|
44
|
+
try {
|
|
45
|
+
const { appName: contextAppName } = useAppConfig();
|
|
46
|
+
appName = contextAppName;
|
|
47
|
+
} catch {
|
|
48
|
+
// Not available, will use undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const fetchAccessLevel = useCallback(async () => {
|
|
52
|
+
if (!userId) {
|
|
53
|
+
setAccessLevel('viewer');
|
|
54
|
+
setIsLoading(false);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
setIsLoading(true);
|
|
60
|
+
setError(null);
|
|
61
|
+
|
|
62
|
+
// Check super admin status first - super admins bypass context requirements
|
|
63
|
+
// This allows super admins to check their access level without organisation context
|
|
64
|
+
const { isSuperAdmin: checkSuperAdmin } = await import('../../api');
|
|
65
|
+
const isSuperAdminUser = await checkSuperAdmin(userId);
|
|
66
|
+
|
|
67
|
+
if (isSuperAdminUser) {
|
|
68
|
+
setAccessLevel('super');
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Early validation: check if scope has required context
|
|
74
|
+
// PORTAL/ADMIN apps allow both contexts to be optional
|
|
75
|
+
if (appName !== 'PORTAL' && appName !== 'ADMIN' && !scope.organisationId && !scope.eventId) {
|
|
76
|
+
const orgError = new OrganisationContextRequiredError();
|
|
77
|
+
setError(orgError);
|
|
78
|
+
setAccessLevel('viewer');
|
|
79
|
+
setIsLoading(false);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const level = await getAccessLevel({ userId, scope }, null, appName);
|
|
84
|
+
setAccessLevel(level);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const error = err instanceof Error ? err : new Error('Failed to fetch access level');
|
|
87
|
+
setError(error);
|
|
88
|
+
setAccessLevel('viewer');
|
|
89
|
+
} finally {
|
|
90
|
+
setIsLoading(false);
|
|
91
|
+
}
|
|
92
|
+
}, [userId, scope.organisationId, scope.eventId, scope.appId, appName]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
fetchAccessLevel();
|
|
96
|
+
}, [fetchAccessLevel]);
|
|
97
|
+
|
|
98
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
99
|
+
return useMemo(() => ({
|
|
100
|
+
accessLevel,
|
|
101
|
+
isLoading,
|
|
102
|
+
error,
|
|
103
|
+
refetch: fetchAccessLevel
|
|
104
|
+
}), [accessLevel, isLoading, error, fetchAccessLevel]);
|
|
105
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { getPermissionMap } from '../../api';
|
|
4
|
+
import { PermissionMap, Scope, UUID } from '../../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to get cached permissions with TTL management
|
|
8
|
+
*
|
|
9
|
+
* @param userId - User ID
|
|
10
|
+
* @param scope - Scope for permission checking
|
|
11
|
+
* @returns Cached permission state and methods
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function MyComponent() {
|
|
16
|
+
* const { permissions, isLoading, error, invalidateCache } = useCachedPermissions(userId, scope);
|
|
17
|
+
*
|
|
18
|
+
* if (isLoading) return <div>Loading cached permissions...</div>;
|
|
19
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
20
|
+
*
|
|
21
|
+
* return (
|
|
22
|
+
* <div>
|
|
23
|
+
* {permissions['read:users'] && <UserList />}
|
|
24
|
+
* <button onClick={invalidateCache}>Refresh Permissions</button>
|
|
25
|
+
* </div>
|
|
26
|
+
* );
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function useCachedPermissions(userId: UUID, scope: Scope): {
|
|
31
|
+
permissions: PermissionMap;
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
error: Error | null;
|
|
34
|
+
invalidateCache: () => void;
|
|
35
|
+
refetch: () => Promise<void>;
|
|
36
|
+
} {
|
|
37
|
+
const [permissions, setPermissions] = useState<PermissionMap>({} as PermissionMap);
|
|
38
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
39
|
+
const [error, setError] = useState<Error | null>(null);
|
|
40
|
+
|
|
41
|
+
const fetchCachedPermissions = useCallback(async () => {
|
|
42
|
+
if (!userId) {
|
|
43
|
+
setPermissions({} as PermissionMap);
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
|
|
52
|
+
const permissionMap = await getPermissionMap({ userId, scope });
|
|
53
|
+
setPermissions(permissionMap);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch cached permissions'));
|
|
56
|
+
} finally {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
}
|
|
59
|
+
}, [userId, scope.organisationId, scope.eventId, scope.appId]);
|
|
60
|
+
|
|
61
|
+
const invalidateCache = useCallback(() => {
|
|
62
|
+
// This would typically invalidate the cache in the actual implementation
|
|
63
|
+
// For now, we'll just refetch
|
|
64
|
+
fetchCachedPermissions();
|
|
65
|
+
}, [fetchCachedPermissions]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
fetchCachedPermissions();
|
|
69
|
+
}, [fetchCachedPermissions]);
|
|
70
|
+
|
|
71
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
72
|
+
return useMemo(() => ({
|
|
73
|
+
permissions,
|
|
74
|
+
isLoading,
|
|
75
|
+
error,
|
|
76
|
+
invalidateCache,
|
|
77
|
+
refetch: fetchCachedPermissions
|
|
78
|
+
}), [permissions, isLoading, error, invalidateCache, fetchCachedPermissions]);
|
|
79
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { isPermitted, isPermittedCached } from '../../api';
|
|
4
|
+
import { getRBACLogger } from '../../config';
|
|
5
|
+
import { Permission, Scope, UUID } from '../../types';
|
|
6
|
+
import { scopeEqual } from '../../utils/deep-equal';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to check if user can perform an action
|
|
10
|
+
*
|
|
11
|
+
* @param userId - User ID
|
|
12
|
+
* @param scope - Scope for permission checking
|
|
13
|
+
* @param permission - Permission to check
|
|
14
|
+
* @param pageId - Optional page ID
|
|
15
|
+
* @param useCache - Whether to use cached results
|
|
16
|
+
* @param appName - Optional app name (for PORTAL/ADMIN special case)
|
|
17
|
+
* @returns Permission check state and methods
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* function MyComponent() {
|
|
22
|
+
* const { can, isLoading, error } = useCan(userId, scope, 'read:users');
|
|
23
|
+
*
|
|
24
|
+
* if (isLoading) return <div>Checking permission...</div>;
|
|
25
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
26
|
+
*
|
|
27
|
+
* return can ? <UserList /> : <div>Access denied</div>;
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useCan(
|
|
32
|
+
userId: UUID,
|
|
33
|
+
scope: Scope,
|
|
34
|
+
permission: Permission,
|
|
35
|
+
pageId?: UUID,
|
|
36
|
+
useCache: boolean = true,
|
|
37
|
+
/**
|
|
38
|
+
* Pre-computed super admin flag to avoid duplicate super admin checks.
|
|
39
|
+
* Callers should check super admin once and pass the result to all useCan hooks.
|
|
40
|
+
* Pass null if not checked yet, false/true if checked.
|
|
41
|
+
* Defaults to null (not checked yet) - hook will check if needed.
|
|
42
|
+
*/
|
|
43
|
+
precomputedSuperAdmin: boolean | null = null,
|
|
44
|
+
appName?: string,
|
|
45
|
+
) {
|
|
46
|
+
const [can, setCan] = useState<boolean>(false);
|
|
47
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
48
|
+
const [error, setError] = useState<Error | null>(null);
|
|
49
|
+
const [isSuperAdmin, setIsSuperAdmin] = useState<boolean | null>(precomputedSuperAdmin ?? null);
|
|
50
|
+
|
|
51
|
+
// Validate scope parameter - handle undefined/null scope gracefully
|
|
52
|
+
const isValidScope = scope && typeof scope === 'object';
|
|
53
|
+
const organisationId = isValidScope ? scope.organisationId : undefined;
|
|
54
|
+
const eventId = isValidScope ? scope.eventId : undefined;
|
|
55
|
+
const appId = isValidScope ? scope.appId : undefined;
|
|
56
|
+
|
|
57
|
+
// Check super-admin status - super admins bypass organisation context requirements
|
|
58
|
+
// PERFORMANCE OPTIMIZATION: Use precomputed value directly - no duplicate checks
|
|
59
|
+
// Callers must check super admin once and pass the result (null if not checked yet)
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
// If precomputed value is null, it means not checked yet - check ourselves
|
|
62
|
+
if (precomputedSuperAdmin === null) {
|
|
63
|
+
if (!userId) {
|
|
64
|
+
setIsSuperAdmin(false);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let cancelled = false;
|
|
69
|
+
const checkSuperAdmin = async () => {
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
try {
|
|
72
|
+
const { isSuperAdmin: checkSuperAdmin } = await import('../../api');
|
|
73
|
+
|
|
74
|
+
// Add timeout warning
|
|
75
|
+
const timeoutWarning = setTimeout(() => {
|
|
76
|
+
if (!cancelled) {
|
|
77
|
+
console.warn('[useCan] Super admin check taking longer than 5 seconds', {
|
|
78
|
+
userId,
|
|
79
|
+
elapsedMs: Date.now() - startTime,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, 5000);
|
|
83
|
+
|
|
84
|
+
const isSuper = await checkSuperAdmin(userId);
|
|
85
|
+
clearTimeout(timeoutWarning);
|
|
86
|
+
|
|
87
|
+
if (!cancelled) {
|
|
88
|
+
const elapsed = Date.now() - startTime;
|
|
89
|
+
if (elapsed > 1000) {
|
|
90
|
+
console.warn('[useCan] Super admin check took longer than expected', {
|
|
91
|
+
userId,
|
|
92
|
+
elapsedMs: elapsed,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
setIsSuperAdmin(isSuper);
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (!cancelled) {
|
|
99
|
+
const elapsed = Date.now() - startTime;
|
|
100
|
+
console.error('[useCan] Error checking super admin', {
|
|
101
|
+
userId,
|
|
102
|
+
error: err,
|
|
103
|
+
elapsedMs: elapsed,
|
|
104
|
+
});
|
|
105
|
+
setIsSuperAdmin(false);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
checkSuperAdmin();
|
|
111
|
+
return () => {
|
|
112
|
+
cancelled = true;
|
|
113
|
+
};
|
|
114
|
+
} else {
|
|
115
|
+
// Precomputed value provided (true/false) - use it directly, no check needed
|
|
116
|
+
setIsSuperAdmin(precomputedSuperAdmin);
|
|
117
|
+
}
|
|
118
|
+
}, [userId, precomputedSuperAdmin]);
|
|
119
|
+
|
|
120
|
+
// Add timeout for missing organisation context (3 seconds)
|
|
121
|
+
// Only apply timeout for resource-level permissions, not page-level (which can handle null orgs)
|
|
122
|
+
// Super admins bypass this check
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
125
|
+
const requiresOrgId = !isPagePermission;
|
|
126
|
+
|
|
127
|
+
// Don't block if user is super-admin (they bypass context requirements)
|
|
128
|
+
if (isSuperAdmin === true) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (requiresOrgId && (!isValidScope || !organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
133
|
+
const timeoutId = setTimeout(() => {
|
|
134
|
+
setError(new Error('Organisation context is required for permission checks'));
|
|
135
|
+
setIsLoading(false);
|
|
136
|
+
setCan(false);
|
|
137
|
+
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
138
|
+
|
|
139
|
+
return () => clearTimeout(timeoutId);
|
|
140
|
+
}
|
|
141
|
+
// Clear error if organisation context becomes available
|
|
142
|
+
if (error?.message === 'Organisation context is required for permission checks') {
|
|
143
|
+
setError(null);
|
|
144
|
+
}
|
|
145
|
+
}, [isValidScope, organisationId, error, permission, pageId, isSuperAdmin]);
|
|
146
|
+
|
|
147
|
+
// Use refs to track the last values to prevent unnecessary re-runs
|
|
148
|
+
const lastUserIdRef = useRef<UUID | null>(null);
|
|
149
|
+
const lastScopeRef = useRef<string | null>(null);
|
|
150
|
+
const lastPermissionRef = useRef<Permission | null>(null);
|
|
151
|
+
const lastPageIdRef = useRef<UUID | undefined | null>(null);
|
|
152
|
+
const lastUseCacheRef = useRef<boolean | null>(null);
|
|
153
|
+
|
|
154
|
+
// Create a stable scope object for comparison
|
|
155
|
+
const stableScope = useMemo(() => {
|
|
156
|
+
if (!isValidScope) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
organisationId,
|
|
161
|
+
eventId,
|
|
162
|
+
appId,
|
|
163
|
+
};
|
|
164
|
+
}, [isValidScope, organisationId, eventId, appId]);
|
|
165
|
+
|
|
166
|
+
// Track previous scope for deep equality comparison
|
|
167
|
+
const prevScopeRef = useRef<Scope | null>(null);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
// Use deep equality check for scope to prevent unnecessary re-runs
|
|
171
|
+
const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
|
|
172
|
+
|
|
173
|
+
// Only run if something has actually changed
|
|
174
|
+
if (
|
|
175
|
+
lastUserIdRef.current !== userId ||
|
|
176
|
+
scopeChanged ||
|
|
177
|
+
lastPermissionRef.current !== permission ||
|
|
178
|
+
lastPageIdRef.current !== pageId ||
|
|
179
|
+
lastUseCacheRef.current !== useCache
|
|
180
|
+
) {
|
|
181
|
+
lastUserIdRef.current = userId;
|
|
182
|
+
prevScopeRef.current = stableScope;
|
|
183
|
+
lastPermissionRef.current = permission;
|
|
184
|
+
lastPageIdRef.current = pageId;
|
|
185
|
+
lastUseCacheRef.current = useCache;
|
|
186
|
+
|
|
187
|
+
// Inline the permission check logic to avoid useCallback dependency issues
|
|
188
|
+
const checkPermission = async () => {
|
|
189
|
+
if (!userId) {
|
|
190
|
+
setCan(false);
|
|
191
|
+
setIsLoading(false);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// CRITICAL: Super admins bypass all permission checks - grant immediately
|
|
196
|
+
// This must be checked BEFORE any other validation to avoid unnecessary API calls
|
|
197
|
+
if (isSuperAdmin === true) {
|
|
198
|
+
setCan(true);
|
|
199
|
+
setIsLoading(false);
|
|
200
|
+
setError(null);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If super admin status is still being checked (null), wait for it to complete
|
|
205
|
+
// Don't proceed with permission check until we know if user is super admin
|
|
206
|
+
if (isSuperAdmin === null) {
|
|
207
|
+
setIsLoading(true);
|
|
208
|
+
setCan(false);
|
|
209
|
+
setError(null);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Validate scope before accessing properties
|
|
214
|
+
if (!isValidScope) {
|
|
215
|
+
setIsLoading(true);
|
|
216
|
+
setCan(false);
|
|
217
|
+
setError(null);
|
|
218
|
+
// Timeout is handled in separate useEffect
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// For page-level permissions, allow undefined/null organisationId (database function handles it)
|
|
223
|
+
// For resource-level permissions, organisationId is required
|
|
224
|
+
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
225
|
+
const requiresOrgId = !isPagePermission;
|
|
226
|
+
|
|
227
|
+
// Check if pageId is a pageName (not a UUID) - if so, we need appId to resolve it
|
|
228
|
+
const isPageName = pageId && typeof pageId === 'string' && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(pageId);
|
|
229
|
+
const needsAppIdForPageName = isPagePermission && isPageName;
|
|
230
|
+
|
|
231
|
+
// Don't check permissions if scope is invalid and orgId is required
|
|
232
|
+
// Wait for organisation context to resolve (unless it's a page permission that can handle null orgs)
|
|
233
|
+
if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
234
|
+
// Not super-admin (already checked above) - wait for org context
|
|
235
|
+
setIsLoading(true);
|
|
236
|
+
setCan(false);
|
|
237
|
+
setError(null);
|
|
238
|
+
// Timeout is handled in separate useEffect (Phase 1.4)
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// For page-level permissions with pageName (not UUID), we need appId to resolve the pageName to pageId
|
|
243
|
+
// Wait for appId to be available before checking permissions
|
|
244
|
+
if (needsAppIdForPageName && (!appId || appId === null || (typeof appId === 'string' && appId.trim() === ''))) {
|
|
245
|
+
setIsLoading(true);
|
|
246
|
+
setCan(false);
|
|
247
|
+
setError(null);
|
|
248
|
+
// Will re-run when appId becomes available (via scope change detection)
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
setIsLoading(true);
|
|
254
|
+
setError(null);
|
|
255
|
+
|
|
256
|
+
// Create a valid scope object for the API call
|
|
257
|
+
// For page-level permissions, organisationId can be undefined (database handles it)
|
|
258
|
+
const validScope: Scope = {
|
|
259
|
+
...(organisationId ? { organisationId } : {}),
|
|
260
|
+
...(eventId ? { eventId } : {}),
|
|
261
|
+
...(appId ? { appId } : {})
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Pass super admin status to avoid duplicate check in isPermitted
|
|
265
|
+
// Note: isPermittedCached doesn't support precomputedSuperAdmin, but the check will be cached
|
|
266
|
+
// If we know user is NOT super admin (isSuperAdmin === false), pass false to skip the check
|
|
267
|
+
const result = useCache
|
|
268
|
+
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
|
|
269
|
+
: await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName, isSuperAdmin === false ? false : null);
|
|
270
|
+
|
|
271
|
+
setCan(result);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const logger = getRBACLogger();
|
|
274
|
+
logger.error('Permission check error:', { permission, error: err });
|
|
275
|
+
console.error('[useCan] Permission check error', { userId, permission, error: err });
|
|
276
|
+
setError(err instanceof Error ? err : new Error('Failed to check permission'));
|
|
277
|
+
setCan(false);
|
|
278
|
+
} finally {
|
|
279
|
+
setIsLoading(false);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
checkPermission();
|
|
284
|
+
}
|
|
285
|
+
}, [userId, stableScope, permission, pageId, useCache, appName, isSuperAdmin]);
|
|
286
|
+
|
|
287
|
+
const refetch = useCallback(async () => {
|
|
288
|
+
if (!userId) {
|
|
289
|
+
setCan(false);
|
|
290
|
+
setIsLoading(false);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate scope before accessing properties
|
|
295
|
+
if (!isValidScope) {
|
|
296
|
+
setCan(false);
|
|
297
|
+
setIsLoading(true);
|
|
298
|
+
setError(null);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// For page-level permissions, allow undefined/null organisationId (database function handles it)
|
|
303
|
+
// For resource-level permissions, organisationId is required
|
|
304
|
+
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
305
|
+
const requiresOrgId = !isPagePermission;
|
|
306
|
+
|
|
307
|
+
// Don't check permissions if scope is invalid and orgId is required
|
|
308
|
+
if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
309
|
+
setCan(false);
|
|
310
|
+
setIsLoading(true);
|
|
311
|
+
setError(null);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
setIsLoading(true);
|
|
317
|
+
setError(null);
|
|
318
|
+
|
|
319
|
+
// Create a valid scope object for the API call
|
|
320
|
+
// For page-level permissions, organisationId can be undefined (database handles it)
|
|
321
|
+
const validScope: Scope = {
|
|
322
|
+
...(organisationId ? { organisationId } : {}),
|
|
323
|
+
...(eventId ? { eventId } : {}),
|
|
324
|
+
...(appId ? { appId } : {})
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const result = useCache
|
|
328
|
+
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
|
|
329
|
+
: await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName, null);
|
|
330
|
+
|
|
331
|
+
setCan(result);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
setError(err instanceof Error ? err : new Error('Failed to check permission'));
|
|
334
|
+
setCan(false);
|
|
335
|
+
} finally {
|
|
336
|
+
setIsLoading(false);
|
|
337
|
+
}
|
|
338
|
+
}, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache, appName]);
|
|
339
|
+
|
|
340
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
341
|
+
return useMemo(() => ({
|
|
342
|
+
can,
|
|
343
|
+
isLoading,
|
|
344
|
+
error,
|
|
345
|
+
refetch
|
|
346
|
+
}), [can, isLoading, error, refetch]);
|
|
347
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { isPermitted, isPermittedCached } from '../../api';
|
|
4
|
+
import { Permission, Scope, UUID } from '../../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to check if user has all of the specified permissions
|
|
8
|
+
*
|
|
9
|
+
* @param userId - User ID
|
|
10
|
+
* @param scope - Scope for permission checking
|
|
11
|
+
* @param permissions - Array of permissions to check
|
|
12
|
+
* @param useCache - Whether to use cached results
|
|
13
|
+
* @returns Whether user has all of the permissions
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function MyComponent() {
|
|
18
|
+
* const { hasAll, isLoading, error } = useHasAllPermissions(
|
|
19
|
+
* userId,
|
|
20
|
+
* scope,
|
|
21
|
+
* ['read:users', 'create:users', 'update:users']
|
|
22
|
+
* );
|
|
23
|
+
*
|
|
24
|
+
* if (isLoading) return <div>Checking permissions...</div>;
|
|
25
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
26
|
+
*
|
|
27
|
+
* return hasAll ? <FullUserManagementPanel /> : <div>Insufficient permissions</div>;
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useHasAllPermissions(
|
|
32
|
+
userId: UUID,
|
|
33
|
+
scope: Scope,
|
|
34
|
+
permissions: Permission[],
|
|
35
|
+
useCache: boolean = true
|
|
36
|
+
): {
|
|
37
|
+
hasAll: boolean;
|
|
38
|
+
isLoading: boolean;
|
|
39
|
+
error: Error | null;
|
|
40
|
+
refetch: () => Promise<void>;
|
|
41
|
+
} {
|
|
42
|
+
const [hasAll, setHasAll] = useState<boolean>(false);
|
|
43
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
44
|
+
const [error, setError] = useState<Error | null>(null);
|
|
45
|
+
|
|
46
|
+
const checkAllPermissions = useCallback(async () => {
|
|
47
|
+
if (!userId || permissions.length === 0) {
|
|
48
|
+
setHasAll(false);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
|
|
57
|
+
let hasAllPermissions = true;
|
|
58
|
+
|
|
59
|
+
for (const permission of permissions) {
|
|
60
|
+
const result = useCache
|
|
61
|
+
? await isPermittedCached({ userId, scope, permission })
|
|
62
|
+
: await isPermitted({ userId, scope, permission }, null, undefined, null);
|
|
63
|
+
|
|
64
|
+
if (!result) {
|
|
65
|
+
hasAllPermissions = false;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setHasAll(hasAllPermissions);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
setError(err instanceof Error ? err : new Error('Failed to check permissions'));
|
|
73
|
+
setHasAll(false);
|
|
74
|
+
} finally {
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
checkAllPermissions();
|
|
81
|
+
}, [checkAllPermissions]);
|
|
82
|
+
|
|
83
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
84
|
+
return useMemo(() => ({
|
|
85
|
+
hasAll,
|
|
86
|
+
isLoading,
|
|
87
|
+
error,
|
|
88
|
+
refetch: checkAllPermissions
|
|
89
|
+
}), [hasAll, isLoading, error, checkAllPermissions]);
|
|
90
|
+
}
|