@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
|
@@ -1,996 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
OrganisationContextRequiredError
|
|
17
|
-
} from '../types';
|
|
18
|
-
import { AccessLevel as AccessLevelType } from '../types';
|
|
19
|
-
import {
|
|
20
|
-
getAccessLevel,
|
|
21
|
-
getPermissionMap,
|
|
22
|
-
isPermitted,
|
|
23
|
-
isPermittedCached
|
|
24
|
-
} from '../api';
|
|
25
|
-
import { getRBACLogger } from '../config';
|
|
26
|
-
import { scopeEqual } from '../utils/deep-equal';
|
|
27
|
-
import { useAppConfig } from '../../hooks/useAppConfig';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Hook to get user's permissions in a scope
|
|
31
|
-
*
|
|
32
|
-
* @param userId - User ID
|
|
33
|
-
* @param organisationId - Organisation ID
|
|
34
|
-
* @param eventId - Event ID (optional)
|
|
35
|
-
* @param appId - Application ID (optional)
|
|
36
|
-
* @returns Permission state and methods
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```tsx
|
|
40
|
-
* function MyComponent() {
|
|
41
|
-
* const { permissions, isLoading, error } = usePermissions(
|
|
42
|
-
* userId,
|
|
43
|
-
* organisationId,
|
|
44
|
-
* eventId,
|
|
45
|
-
* appId
|
|
46
|
-
* );
|
|
47
|
-
*
|
|
48
|
-
* if (isLoading) return <div>Loading...</div>;
|
|
49
|
-
* if (error) return <div>Error: {error.message}</div>;
|
|
50
|
-
*
|
|
51
|
-
* return (
|
|
52
|
-
* <div>
|
|
53
|
-
* {permissions['read:users'] && <UserList />}
|
|
54
|
-
* {permissions['create:users'] && <CreateUserButton />}
|
|
55
|
-
* </div>
|
|
56
|
-
* );
|
|
57
|
-
* }
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
export function usePermissions(
|
|
61
|
-
userId: UUID,
|
|
62
|
-
organisationId: string | undefined,
|
|
63
|
-
eventId: string | undefined,
|
|
64
|
-
appId: string | undefined
|
|
65
|
-
) {
|
|
66
|
-
const [permissions, setPermissions] = useState<PermissionMap>({} as PermissionMap);
|
|
67
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
68
|
-
const [error, setError] = useState<Error | null>(null);
|
|
69
|
-
const [fetchTrigger, setFetchTrigger] = useState(0);
|
|
70
|
-
const isFetchingRef = useRef(false);
|
|
71
|
-
const logger = getRBACLogger();
|
|
72
|
-
|
|
73
|
-
// Track previous values to detect changes imperatively
|
|
74
|
-
const prevValuesRef = useRef({ userId, organisationId, eventId, appId });
|
|
75
|
-
|
|
76
|
-
// Normalize organisationId to empty string if undefined
|
|
77
|
-
const orgId = organisationId || '';
|
|
78
|
-
|
|
79
|
-
// Removed excessive logging - only log when scope actually changes (not on every render)
|
|
80
|
-
|
|
81
|
-
// Add timeout for missing organisation context (3 seconds)
|
|
82
|
-
// OPTIMIZATION: Skip timeout if userId is null/undefined (indicates pre-filtered mode)
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
// If userId is null/undefined, skip the timeout - this indicates items are pre-filtered
|
|
85
|
-
// and we don't need to wait for organisation context
|
|
86
|
-
if (!userId) {
|
|
87
|
-
return; // Skip timeout when userId is null (pre-filtered mode)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
|
|
91
|
-
const timeoutId = setTimeout(() => {
|
|
92
|
-
setError(new Error('Organisation context is required for permission checks'));
|
|
93
|
-
setIsLoading(false);
|
|
94
|
-
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
95
|
-
|
|
96
|
-
return () => clearTimeout(timeoutId);
|
|
97
|
-
}
|
|
98
|
-
// Clear error if organisation context becomes available
|
|
99
|
-
if (error?.message === 'Organisation context is required for permission checks') {
|
|
100
|
-
setError(null);
|
|
101
|
-
}
|
|
102
|
-
}, [userId, organisationId, error, orgId]);
|
|
103
|
-
|
|
104
|
-
// CRITICAL: Detect parameter changes and trigger fetch
|
|
105
|
-
// Moved to useEffect to prevent render-time state updates that could cause render loops
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
const paramsChanged =
|
|
108
|
-
prevValuesRef.current.userId !== userId ||
|
|
109
|
-
prevValuesRef.current.organisationId !== organisationId ||
|
|
110
|
-
prevValuesRef.current.eventId !== eventId ||
|
|
111
|
-
prevValuesRef.current.appId !== appId;
|
|
112
|
-
|
|
113
|
-
if (paramsChanged) {
|
|
114
|
-
// Only log significant changes (appId changes are most important)
|
|
115
|
-
if (prevValuesRef.current.appId !== appId) {
|
|
116
|
-
// AppId changed - triggering fetch
|
|
117
|
-
}
|
|
118
|
-
prevValuesRef.current = { userId, organisationId, eventId, appId };
|
|
119
|
-
// Increment counter to force fetch useEffect to run
|
|
120
|
-
setFetchTrigger(prev => prev + 1);
|
|
121
|
-
}
|
|
122
|
-
}, [userId, organisationId, eventId, appId, logger]);
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
const fetchPermissions = async () => {
|
|
126
|
-
// Prevent multiple simultaneous fetches
|
|
127
|
-
if (isFetchingRef.current) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!userId) {
|
|
132
|
-
setPermissions({} as PermissionMap);
|
|
133
|
-
setIsLoading(false);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
138
|
-
// Wait for organisation context to resolve
|
|
139
|
-
// IMPORTANT: Don't clear existing permissions here - keep them until we have new ones
|
|
140
|
-
// OPTIMIZATION: If userId is null/undefined, immediately set loading to false
|
|
141
|
-
// This indicates pre-filtered mode where we don't need to wait for organisation context
|
|
142
|
-
if (!userId) {
|
|
143
|
-
setPermissions({} as PermissionMap);
|
|
144
|
-
setIsLoading(false);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
|
|
149
|
-
// Keep existing permissions, just mark as loading
|
|
150
|
-
setIsLoading(true);
|
|
151
|
-
setError(null);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
isFetchingRef.current = true;
|
|
157
|
-
setIsLoading(true);
|
|
158
|
-
setError(null);
|
|
159
|
-
|
|
160
|
-
// Build scope object for API call
|
|
161
|
-
const scope: Scope = {
|
|
162
|
-
organisationId: orgId,
|
|
163
|
-
eventId: eventId,
|
|
164
|
-
appId: appId
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Fetch new permissions - don't clear old ones until we have new ones
|
|
168
|
-
const permissionMap = await getPermissionMap({ userId, scope });
|
|
169
|
-
|
|
170
|
-
// Only log if there's a significant change or error
|
|
171
|
-
const permissionCount = Object.keys(permissionMap).length;
|
|
172
|
-
if (permissionCount === 0 && Object.keys(permissions).length > 0) {
|
|
173
|
-
logger.warn('[usePermissions] Permissions fetched but returned empty map', {
|
|
174
|
-
scope: { organisationId: orgId, eventId, appId }
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Only update permissions if fetch was successful
|
|
179
|
-
setPermissions(permissionMap);
|
|
180
|
-
} catch (err) {
|
|
181
|
-
// On error, keep existing permissions but set error state
|
|
182
|
-
// This prevents the UI from losing all items when there's a transient error
|
|
183
|
-
logger.error('[usePermissions] Failed to fetch permissions:', err);
|
|
184
|
-
setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
|
|
185
|
-
// Don't clear permissions on error - keep what we had
|
|
186
|
-
} finally {
|
|
187
|
-
setIsLoading(false);
|
|
188
|
-
isFetchingRef.current = false;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
fetchPermissions();
|
|
193
|
-
}, [fetchTrigger, userId, organisationId, eventId, appId]);
|
|
194
|
-
|
|
195
|
-
const hasPermission = useCallback((permission: Permission): boolean => {
|
|
196
|
-
if (permissions['*']) {
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
return permissions[permission] === true;
|
|
200
|
-
}, [permissions]);
|
|
201
|
-
|
|
202
|
-
const hasAnyPermission = useCallback((permissionList: Permission[]): boolean => {
|
|
203
|
-
if (permissions['*']) {
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
return permissionList.some(p => permissions[p] === true);
|
|
207
|
-
}, [permissions]);
|
|
208
|
-
|
|
209
|
-
const hasAllPermissions = useCallback((permissionList: Permission[]): boolean => {
|
|
210
|
-
if (permissions['*']) {
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
return permissionList.every(p => permissions[p] === true);
|
|
214
|
-
}, [permissions]);
|
|
215
|
-
|
|
216
|
-
const refetch = useCallback(async () => {
|
|
217
|
-
// Prevent multiple simultaneous fetches
|
|
218
|
-
if (isFetchingRef.current) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (!userId) {
|
|
223
|
-
setPermissions({} as PermissionMap);
|
|
224
|
-
setIsLoading(false);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
229
|
-
// IMPORTANT: Don't clear existing permissions - keep them until we have new ones
|
|
230
|
-
if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
|
|
231
|
-
// Keep existing permissions, just mark as loading
|
|
232
|
-
setIsLoading(true);
|
|
233
|
-
setError(null);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
isFetchingRef.current = true;
|
|
239
|
-
setIsLoading(true);
|
|
240
|
-
setError(null);
|
|
241
|
-
|
|
242
|
-
// Build scope object for API call
|
|
243
|
-
const scope: Scope = {
|
|
244
|
-
organisationId: orgId,
|
|
245
|
-
eventId: eventId,
|
|
246
|
-
appId: appId
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// Fetch new permissions - don't clear old ones until we have new ones
|
|
250
|
-
const permissionMap = await getPermissionMap({ userId, scope });
|
|
251
|
-
|
|
252
|
-
// Only update permissions if fetch was successful
|
|
253
|
-
setPermissions(permissionMap);
|
|
254
|
-
} catch (err) {
|
|
255
|
-
// On error, keep existing permissions but set error state
|
|
256
|
-
// This prevents the UI from losing all items when there's a transient error
|
|
257
|
-
const logger = getRBACLogger();
|
|
258
|
-
logger.error('Failed to refetch permissions:', err);
|
|
259
|
-
setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
|
|
260
|
-
// Don't clear permissions on error - keep what we had
|
|
261
|
-
} finally {
|
|
262
|
-
setIsLoading(false);
|
|
263
|
-
isFetchingRef.current = false;
|
|
264
|
-
}
|
|
265
|
-
}, [userId, organisationId, eventId, appId]);
|
|
266
|
-
|
|
267
|
-
// Memoize the return object to prevent unnecessary re-renders
|
|
268
|
-
return useMemo(() => ({
|
|
269
|
-
permissions,
|
|
270
|
-
isLoading,
|
|
271
|
-
error,
|
|
272
|
-
hasPermission,
|
|
273
|
-
hasAnyPermission,
|
|
274
|
-
hasAllPermissions,
|
|
275
|
-
refetch
|
|
276
|
-
}), [permissions, isLoading, error, hasPermission, hasAnyPermission, hasAllPermissions, refetch]);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Hook to check if user can perform an action
|
|
281
|
-
*
|
|
282
|
-
* @param userId - User ID
|
|
283
|
-
* @param scope - Scope for permission checking
|
|
284
|
-
* @param permission - Permission to check
|
|
285
|
-
* @param pageId - Optional page ID
|
|
286
|
-
* @param useCache - Whether to use cached results
|
|
287
|
-
* @param appName - Optional app name (for PORTAL/ADMIN special case)
|
|
288
|
-
* @returns Permission check state and methods
|
|
289
|
-
*
|
|
290
|
-
* @example
|
|
291
|
-
* ```tsx
|
|
292
|
-
* function MyComponent() {
|
|
293
|
-
* const { can, isLoading, error } = useCan(userId, scope, 'read:users');
|
|
294
|
-
*
|
|
295
|
-
* if (isLoading) return <div>Checking permission...</div>;
|
|
296
|
-
* if (error) return <div>Error: {error.message}</div>;
|
|
297
|
-
*
|
|
298
|
-
* return can ? <UserList /> : <div>Access denied</div>;
|
|
299
|
-
* }
|
|
300
|
-
* ```
|
|
301
|
-
*/
|
|
302
|
-
export function useCan(
|
|
303
|
-
userId: UUID,
|
|
304
|
-
scope: Scope,
|
|
305
|
-
permission: Permission,
|
|
306
|
-
pageId?: UUID,
|
|
307
|
-
useCache: boolean = true,
|
|
308
|
-
appName?: string
|
|
309
|
-
) {
|
|
310
|
-
const [can, setCan] = useState<boolean>(false);
|
|
311
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
312
|
-
const [error, setError] = useState<Error | null>(null);
|
|
313
|
-
const [isSuperAdmin, setIsSuperAdmin] = useState<boolean | null>(null);
|
|
314
|
-
|
|
315
|
-
// Validate scope parameter - handle undefined/null scope gracefully
|
|
316
|
-
const isValidScope = scope && typeof scope === 'object';
|
|
317
|
-
const organisationId = isValidScope ? scope.organisationId : undefined;
|
|
318
|
-
const eventId = isValidScope ? scope.eventId : undefined;
|
|
319
|
-
const appId = isValidScope ? scope.appId : undefined;
|
|
320
|
-
|
|
321
|
-
// Check super-admin status - super admins bypass organisation context requirements
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
if (!userId) {
|
|
324
|
-
setIsSuperAdmin(false);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
let cancelled = false;
|
|
329
|
-
const checkSuperAdmin = async () => {
|
|
330
|
-
try {
|
|
331
|
-
const { isSuperAdmin: checkSuperAdmin } = await import('../api');
|
|
332
|
-
const isSuper = await checkSuperAdmin(userId);
|
|
333
|
-
if (!cancelled) {
|
|
334
|
-
setIsSuperAdmin(isSuper);
|
|
335
|
-
}
|
|
336
|
-
} catch (err) {
|
|
337
|
-
if (!cancelled) {
|
|
338
|
-
setIsSuperAdmin(false);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
checkSuperAdmin();
|
|
344
|
-
return () => {
|
|
345
|
-
cancelled = true;
|
|
346
|
-
};
|
|
347
|
-
}, [userId]);
|
|
348
|
-
|
|
349
|
-
// Add timeout for missing organisation context (3 seconds)
|
|
350
|
-
// Only apply timeout for resource-level permissions, not page-level (which can handle null orgs)
|
|
351
|
-
// Super admins bypass this check
|
|
352
|
-
useEffect(() => {
|
|
353
|
-
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
354
|
-
const requiresOrgId = !isPagePermission;
|
|
355
|
-
|
|
356
|
-
// Don't block if user is super-admin (they bypass context requirements)
|
|
357
|
-
if (isSuperAdmin === true) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (requiresOrgId && (!isValidScope || !organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
362
|
-
const timeoutId = setTimeout(() => {
|
|
363
|
-
setError(new Error('Organisation context is required for permission checks'));
|
|
364
|
-
setIsLoading(false);
|
|
365
|
-
setCan(false);
|
|
366
|
-
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
367
|
-
|
|
368
|
-
return () => clearTimeout(timeoutId);
|
|
369
|
-
}
|
|
370
|
-
// Clear error if organisation context becomes available
|
|
371
|
-
if (error?.message === 'Organisation context is required for permission checks') {
|
|
372
|
-
setError(null);
|
|
373
|
-
}
|
|
374
|
-
}, [isValidScope, organisationId, error, permission, pageId, isSuperAdmin]);
|
|
375
|
-
|
|
376
|
-
// Use refs to track the last values to prevent unnecessary re-runs
|
|
377
|
-
const lastUserIdRef = useRef<UUID | null>(null);
|
|
378
|
-
const lastScopeRef = useRef<string | null>(null);
|
|
379
|
-
const lastPermissionRef = useRef<Permission | null>(null);
|
|
380
|
-
const lastPageIdRef = useRef<UUID | undefined | null>(null);
|
|
381
|
-
const lastUseCacheRef = useRef<boolean | null>(null);
|
|
382
|
-
|
|
383
|
-
// Create a stable scope object for comparison
|
|
384
|
-
const stableScope = useMemo(() => {
|
|
385
|
-
if (!isValidScope) {
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
return {
|
|
389
|
-
organisationId,
|
|
390
|
-
eventId,
|
|
391
|
-
appId,
|
|
392
|
-
};
|
|
393
|
-
}, [isValidScope, organisationId, eventId, appId]);
|
|
394
|
-
|
|
395
|
-
// Track previous scope for deep equality comparison
|
|
396
|
-
const prevScopeRef = useRef<Scope | null>(null);
|
|
397
|
-
|
|
398
|
-
useEffect(() => {
|
|
399
|
-
// Use deep equality check for scope to prevent unnecessary re-runs
|
|
400
|
-
const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
|
|
401
|
-
|
|
402
|
-
// Only run if something has actually changed
|
|
403
|
-
if (
|
|
404
|
-
lastUserIdRef.current !== userId ||
|
|
405
|
-
scopeChanged ||
|
|
406
|
-
lastPermissionRef.current !== permission ||
|
|
407
|
-
lastPageIdRef.current !== pageId ||
|
|
408
|
-
lastUseCacheRef.current !== useCache
|
|
409
|
-
) {
|
|
410
|
-
lastUserIdRef.current = userId;
|
|
411
|
-
prevScopeRef.current = stableScope;
|
|
412
|
-
lastPermissionRef.current = permission;
|
|
413
|
-
lastPageIdRef.current = pageId;
|
|
414
|
-
lastUseCacheRef.current = useCache;
|
|
415
|
-
|
|
416
|
-
// Inline the permission check logic to avoid useCallback dependency issues
|
|
417
|
-
const checkPermission = async () => {
|
|
418
|
-
if (!userId) {
|
|
419
|
-
setCan(false);
|
|
420
|
-
setIsLoading(false);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Validate scope before accessing properties
|
|
425
|
-
if (!isValidScope) {
|
|
426
|
-
setIsLoading(true);
|
|
427
|
-
setCan(false);
|
|
428
|
-
setError(null);
|
|
429
|
-
// Timeout is handled in separate useEffect
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// For page-level permissions, allow undefined/null organisationId (database function handles it)
|
|
434
|
-
// For resource-level permissions, organisationId is required
|
|
435
|
-
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
436
|
-
const requiresOrgId = !isPagePermission;
|
|
437
|
-
|
|
438
|
-
// Check if pageId is a pageName (not a UUID) - if so, we need appId to resolve it
|
|
439
|
-
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);
|
|
440
|
-
const needsAppIdForPageName = isPagePermission && isPageName;
|
|
441
|
-
|
|
442
|
-
// Don't check permissions if scope is invalid and orgId is required
|
|
443
|
-
// Wait for organisation context to resolve (unless it's a page permission that can handle null orgs)
|
|
444
|
-
// Super admins bypass this check - only proceed if super-admin status is confirmed to be true
|
|
445
|
-
// If super-admin status is still being checked (null), wait for it to complete
|
|
446
|
-
if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
447
|
-
// Only proceed if user is confirmed to be super-admin
|
|
448
|
-
if (isSuperAdmin === true) {
|
|
449
|
-
// Super-admin bypass - allow check to proceed (isPermitted will handle super-admin bypass)
|
|
450
|
-
} else {
|
|
451
|
-
// Not super-admin or still checking - wait for org context or super-admin check to complete
|
|
452
|
-
setIsLoading(true);
|
|
453
|
-
setCan(false);
|
|
454
|
-
setError(null);
|
|
455
|
-
// Timeout is handled in separate useEffect (Phase 1.4)
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// For page-level permissions with pageName (not UUID), we need appId to resolve the pageName to pageId
|
|
461
|
-
// Wait for appId to be available before checking permissions
|
|
462
|
-
if (needsAppIdForPageName && (!appId || appId === null || (typeof appId === 'string' && appId.trim() === ''))) {
|
|
463
|
-
setIsLoading(true);
|
|
464
|
-
setCan(false);
|
|
465
|
-
setError(null);
|
|
466
|
-
// Will re-run when appId becomes available (via scope change detection)
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
try {
|
|
471
|
-
setIsLoading(true);
|
|
472
|
-
setError(null);
|
|
473
|
-
|
|
474
|
-
// Create a valid scope object for the API call
|
|
475
|
-
// For page-level permissions, organisationId can be undefined (database handles it)
|
|
476
|
-
const validScope: Scope = {
|
|
477
|
-
...(organisationId ? { organisationId } : {}),
|
|
478
|
-
...(eventId ? { eventId } : {}),
|
|
479
|
-
...(appId ? { appId } : {})
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
const result = useCache
|
|
483
|
-
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
|
|
484
|
-
: await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName);
|
|
485
|
-
|
|
486
|
-
setCan(result);
|
|
487
|
-
} catch (err) {
|
|
488
|
-
const logger = getRBACLogger();
|
|
489
|
-
logger.error('Permission check error:', { permission, error: err });
|
|
490
|
-
setError(err instanceof Error ? err : new Error('Failed to check permission'));
|
|
491
|
-
setCan(false);
|
|
492
|
-
} finally {
|
|
493
|
-
setIsLoading(false);
|
|
494
|
-
}
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
checkPermission();
|
|
498
|
-
}
|
|
499
|
-
}, [userId, stableScope, permission, pageId, useCache, appName, isSuperAdmin]);
|
|
500
|
-
|
|
501
|
-
const refetch = useCallback(async () => {
|
|
502
|
-
if (!userId) {
|
|
503
|
-
setCan(false);
|
|
504
|
-
setIsLoading(false);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Validate scope before accessing properties
|
|
509
|
-
if (!isValidScope) {
|
|
510
|
-
setCan(false);
|
|
511
|
-
setIsLoading(true);
|
|
512
|
-
setError(null);
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// For page-level permissions, allow undefined/null organisationId (database function handles it)
|
|
517
|
-
// For resource-level permissions, organisationId is required
|
|
518
|
-
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
519
|
-
const requiresOrgId = !isPagePermission;
|
|
520
|
-
|
|
521
|
-
// Don't check permissions if scope is invalid and orgId is required
|
|
522
|
-
if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
523
|
-
setCan(false);
|
|
524
|
-
setIsLoading(true);
|
|
525
|
-
setError(null);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
try {
|
|
530
|
-
setIsLoading(true);
|
|
531
|
-
setError(null);
|
|
532
|
-
|
|
533
|
-
// Create a valid scope object for the API call
|
|
534
|
-
// For page-level permissions, organisationId can be undefined (database handles it)
|
|
535
|
-
const validScope: Scope = {
|
|
536
|
-
...(organisationId ? { organisationId } : {}),
|
|
537
|
-
...(eventId ? { eventId } : {}),
|
|
538
|
-
...(appId ? { appId } : {})
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
const result = useCache
|
|
542
|
-
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
|
|
543
|
-
: await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName);
|
|
544
|
-
|
|
545
|
-
setCan(result);
|
|
546
|
-
} catch (err) {
|
|
547
|
-
setError(err instanceof Error ? err : new Error('Failed to check permission'));
|
|
548
|
-
setCan(false);
|
|
549
|
-
} finally {
|
|
550
|
-
setIsLoading(false);
|
|
551
|
-
}
|
|
552
|
-
}, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache, appName]);
|
|
553
|
-
|
|
554
|
-
// Memoize the return object to prevent unnecessary re-renders
|
|
555
|
-
return useMemo(() => ({
|
|
556
|
-
can,
|
|
557
|
-
isLoading,
|
|
558
|
-
error,
|
|
559
|
-
refetch
|
|
560
|
-
}), [can, isLoading, error, refetch]);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Hook to get user's access level in a scope
|
|
565
|
-
*
|
|
566
|
-
* @param userId - User ID
|
|
567
|
-
* @param scope - Scope for access level checking
|
|
568
|
-
* @returns Access level state and methods
|
|
569
|
-
*
|
|
570
|
-
* @example
|
|
571
|
-
* ```tsx
|
|
572
|
-
* function MyComponent() {
|
|
573
|
-
* const { accessLevel, isLoading, error } = useAccessLevel(userId, scope);
|
|
574
|
-
*
|
|
575
|
-
* if (isLoading) return <div>Loading access level...</div>;
|
|
576
|
-
* if (error) return <div>Error: {error.message}</div>;
|
|
577
|
-
*
|
|
578
|
-
* return (
|
|
579
|
-
* <div>
|
|
580
|
-
* Access Level: {accessLevel}
|
|
581
|
-
* {accessLevel >= AccessLevel.ADMIN && <AdminPanel />}
|
|
582
|
-
* </div>
|
|
583
|
-
* );
|
|
584
|
-
* }
|
|
585
|
-
* ```
|
|
586
|
-
*/
|
|
587
|
-
export function useAccessLevel(userId: UUID, scope: Scope): {
|
|
588
|
-
accessLevel: AccessLevelType;
|
|
589
|
-
isLoading: boolean;
|
|
590
|
-
error: Error | null;
|
|
591
|
-
refetch: () => Promise<void>;
|
|
592
|
-
} {
|
|
593
|
-
const [accessLevel, setAccessLevel] = useState<AccessLevelType>('viewer');
|
|
594
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
595
|
-
const [error, setError] = useState<Error | null>(null);
|
|
596
|
-
|
|
597
|
-
// Get appName from context if available (safely handles missing context)
|
|
598
|
-
let appName: string | undefined;
|
|
599
|
-
try {
|
|
600
|
-
const { appName: contextAppName } = useAppConfig();
|
|
601
|
-
appName = contextAppName;
|
|
602
|
-
} catch {
|
|
603
|
-
// Not available, will use undefined
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const fetchAccessLevel = useCallback(async () => {
|
|
607
|
-
if (!userId) {
|
|
608
|
-
setAccessLevel('viewer');
|
|
609
|
-
setIsLoading(false);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
try {
|
|
614
|
-
setIsLoading(true);
|
|
615
|
-
setError(null);
|
|
616
|
-
|
|
617
|
-
// Check super admin status first - super admins bypass context requirements
|
|
618
|
-
// This allows super admins to check their access level without organisation context
|
|
619
|
-
const { isSuperAdmin: checkSuperAdmin } = await import('../api');
|
|
620
|
-
const isSuperAdminUser = await checkSuperAdmin(userId);
|
|
621
|
-
|
|
622
|
-
if (isSuperAdminUser) {
|
|
623
|
-
setAccessLevel('super');
|
|
624
|
-
setIsLoading(false);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Early validation: check if scope has required context
|
|
629
|
-
// PORTAL/ADMIN apps allow both contexts to be optional
|
|
630
|
-
if (appName !== 'PORTAL' && appName !== 'ADMIN' && !scope.organisationId && !scope.eventId) {
|
|
631
|
-
const orgError = new OrganisationContextRequiredError();
|
|
632
|
-
setError(orgError);
|
|
633
|
-
setAccessLevel('viewer');
|
|
634
|
-
setIsLoading(false);
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const level = await getAccessLevel({ userId, scope }, null, appName);
|
|
639
|
-
setAccessLevel(level);
|
|
640
|
-
} catch (err) {
|
|
641
|
-
const error = err instanceof Error ? err : new Error('Failed to fetch access level');
|
|
642
|
-
setError(error);
|
|
643
|
-
setAccessLevel('viewer');
|
|
644
|
-
} finally {
|
|
645
|
-
setIsLoading(false);
|
|
646
|
-
}
|
|
647
|
-
}, [userId, scope.organisationId, scope.eventId, scope.appId, appName]);
|
|
648
|
-
|
|
649
|
-
useEffect(() => {
|
|
650
|
-
fetchAccessLevel();
|
|
651
|
-
}, [fetchAccessLevel]);
|
|
652
|
-
|
|
653
|
-
// Memoize the return object to prevent unnecessary re-renders
|
|
654
|
-
return useMemo(() => ({
|
|
655
|
-
accessLevel,
|
|
656
|
-
isLoading,
|
|
657
|
-
error,
|
|
658
|
-
refetch: fetchAccessLevel
|
|
659
|
-
}), [accessLevel, isLoading, error, fetchAccessLevel]);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Hook to check multiple permissions at once
|
|
664
|
-
*
|
|
665
|
-
* @param userId - User ID
|
|
666
|
-
* @param scope - Scope for permission checking
|
|
667
|
-
* @param permissions - Array of permissions to check
|
|
668
|
-
* @param useCache - Whether to use cached results
|
|
669
|
-
* @returns Multiple permission check results
|
|
670
|
-
*
|
|
671
|
-
* @example
|
|
672
|
-
* ```tsx
|
|
673
|
-
* function MyComponent() {
|
|
674
|
-
* const { results, isLoading, error } = useMultiplePermissions(
|
|
675
|
-
* userId,
|
|
676
|
-
* scope,
|
|
677
|
-
* ['read:users', 'create:users', 'update:users']
|
|
678
|
-
* );
|
|
679
|
-
*
|
|
680
|
-
* if (isLoading) return <div>Checking permissions...</div>;
|
|
681
|
-
* if (error) return <div>Error: {error.message}</div>;
|
|
682
|
-
*
|
|
683
|
-
* return (
|
|
684
|
-
* <div>
|
|
685
|
-
* {results['read:users'] && <UserList />}
|
|
686
|
-
* {results['create:users'] && <CreateUserButton />}
|
|
687
|
-
* {results['update:users'] && <EditUserButton />}
|
|
688
|
-
* </div>
|
|
689
|
-
* );
|
|
690
|
-
* }
|
|
691
|
-
* ```
|
|
692
|
-
*/
|
|
693
|
-
export function useMultiplePermissions(
|
|
694
|
-
userId: UUID,
|
|
695
|
-
scope: Scope,
|
|
696
|
-
permissions: Permission[],
|
|
697
|
-
useCache: boolean = true
|
|
698
|
-
): {
|
|
699
|
-
results: Record<Permission, boolean>;
|
|
700
|
-
isLoading: boolean;
|
|
701
|
-
error: Error | null;
|
|
702
|
-
refetch: () => Promise<void>;
|
|
703
|
-
} {
|
|
704
|
-
const [results, setResults] = useState<Record<Permission, boolean>>({} as Record<Permission, boolean>);
|
|
705
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
706
|
-
const [error, setError] = useState<Error | null>(null);
|
|
707
|
-
|
|
708
|
-
const checkPermissions = useCallback(async () => {
|
|
709
|
-
if (!userId || permissions.length === 0) {
|
|
710
|
-
setResults({} as Record<Permission, boolean>);
|
|
711
|
-
setIsLoading(false);
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
try {
|
|
716
|
-
setIsLoading(true);
|
|
717
|
-
setError(null);
|
|
718
|
-
|
|
719
|
-
const permissionResults: Record<Permission, boolean> = {} as Record<Permission, boolean>;
|
|
720
|
-
|
|
721
|
-
// Check each permission
|
|
722
|
-
for (const permission of permissions) {
|
|
723
|
-
const result = useCache
|
|
724
|
-
? await isPermittedCached({ userId, scope, permission })
|
|
725
|
-
: await isPermitted({ userId, scope, permission });
|
|
726
|
-
permissionResults[permission] = result;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
setResults(permissionResults);
|
|
730
|
-
} catch (err) {
|
|
731
|
-
setError(err instanceof Error ? err : new Error('Failed to check permissions'));
|
|
732
|
-
setResults({} as Record<Permission, boolean>);
|
|
733
|
-
} finally {
|
|
734
|
-
setIsLoading(false);
|
|
735
|
-
}
|
|
736
|
-
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
737
|
-
|
|
738
|
-
useEffect(() => {
|
|
739
|
-
checkPermissions();
|
|
740
|
-
}, [checkPermissions]);
|
|
741
|
-
|
|
742
|
-
// Memoize the return object to prevent unnecessary re-renders
|
|
743
|
-
return useMemo(() => ({
|
|
744
|
-
results,
|
|
745
|
-
isLoading,
|
|
746
|
-
error,
|
|
747
|
-
refetch: checkPermissions
|
|
748
|
-
}), [results, isLoading, error, checkPermissions]);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Hook to check if user has any of the specified permissions
|
|
753
|
-
*
|
|
754
|
-
* @param userId - User ID
|
|
755
|
-
* @param scope - Scope for permission checking
|
|
756
|
-
* @param permissions - Array of permissions to check
|
|
757
|
-
* @param useCache - Whether to use cached results
|
|
758
|
-
* @returns Whether user has any of the permissions
|
|
759
|
-
*
|
|
760
|
-
* @example
|
|
761
|
-
* ```tsx
|
|
762
|
-
* function MyComponent() {
|
|
763
|
-
* const { hasAny, isLoading, error } = useHasAnyPermission(
|
|
764
|
-
* userId,
|
|
765
|
-
* scope,
|
|
766
|
-
* ['read:users', 'create:users']
|
|
767
|
-
* );
|
|
768
|
-
*
|
|
769
|
-
* if (isLoading) return <div>Checking permissions...</div>;
|
|
770
|
-
* if (error) return <div>Error: {error.message}</div>;
|
|
771
|
-
*
|
|
772
|
-
* return hasAny ? <UserManagementPanel /> : <div>No user permissions</div>;
|
|
773
|
-
* }
|
|
774
|
-
* ```
|
|
775
|
-
*/
|
|
776
|
-
export function useHasAnyPermission(
|
|
777
|
-
userId: UUID,
|
|
778
|
-
scope: Scope,
|
|
779
|
-
permissions: Permission[],
|
|
780
|
-
useCache: boolean = true
|
|
781
|
-
): {
|
|
782
|
-
hasAny: boolean;
|
|
783
|
-
isLoading: boolean;
|
|
784
|
-
error: Error | null;
|
|
785
|
-
refetch: () => Promise<void>;
|
|
786
|
-
} {
|
|
787
|
-
const [hasAny, setHasAny] = useState<boolean>(false);
|
|
788
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
789
|
-
const [error, setError] = useState<Error | null>(null);
|
|
790
|
-
|
|
791
|
-
const checkAnyPermission = useCallback(async () => {
|
|
792
|
-
if (!userId || permissions.length === 0) {
|
|
793
|
-
setHasAny(false);
|
|
794
|
-
setIsLoading(false);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
try {
|
|
799
|
-
setIsLoading(true);
|
|
800
|
-
setError(null);
|
|
801
|
-
|
|
802
|
-
let hasAnyPermission = false;
|
|
803
|
-
|
|
804
|
-
for (const permission of permissions) {
|
|
805
|
-
const result = useCache
|
|
806
|
-
? await isPermittedCached({ userId, scope, permission })
|
|
807
|
-
: await isPermitted({ userId, scope, permission });
|
|
808
|
-
|
|
809
|
-
if (result) {
|
|
810
|
-
hasAnyPermission = true;
|
|
811
|
-
break;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
setHasAny(hasAnyPermission);
|
|
816
|
-
} catch (err) {
|
|
817
|
-
setError(err instanceof Error ? err : new Error('Failed to check permissions'));
|
|
818
|
-
setHasAny(false);
|
|
819
|
-
} finally {
|
|
820
|
-
setIsLoading(false);
|
|
821
|
-
}
|
|
822
|
-
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
823
|
-
|
|
824
|
-
useEffect(() => {
|
|
825
|
-
checkAnyPermission();
|
|
826
|
-
}, [checkAnyPermission]);
|
|
827
|
-
|
|
828
|
-
// Memoize the return object to prevent unnecessary re-renders
|
|
829
|
-
return useMemo(() => ({
|
|
830
|
-
hasAny,
|
|
831
|
-
isLoading,
|
|
832
|
-
error,
|
|
833
|
-
refetch: checkAnyPermission
|
|
834
|
-
}), [hasAny, isLoading, error, checkAnyPermission]);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* Hook to check if user has all of the specified permissions
|
|
839
|
-
*
|
|
840
|
-
* @param userId - User ID
|
|
841
|
-
* @param scope - Scope for permission checking
|
|
842
|
-
* @param permissions - Array of permissions to check
|
|
843
|
-
* @param useCache - Whether to use cached results
|
|
844
|
-
* @returns Whether user has all of the permissions
|
|
845
|
-
*
|
|
846
|
-
* @example
|
|
847
|
-
* ```tsx
|
|
848
|
-
* function MyComponent() {
|
|
849
|
-
* const { hasAll, isLoading, error } = useHasAllPermissions(
|
|
850
|
-
* userId,
|
|
851
|
-
* scope,
|
|
852
|
-
* ['read:users', 'create:users', 'update:users']
|
|
853
|
-
* );
|
|
854
|
-
*
|
|
855
|
-
* if (isLoading) return <div>Checking permissions...</div>;
|
|
856
|
-
* if (error) return <div>Error: {error.message}</div>;
|
|
857
|
-
*
|
|
858
|
-
* return hasAll ? <FullUserManagementPanel /> : <div>Insufficient permissions</div>;
|
|
859
|
-
* }
|
|
860
|
-
* ```
|
|
861
|
-
*/
|
|
862
|
-
export function useHasAllPermissions(
|
|
863
|
-
userId: UUID,
|
|
864
|
-
scope: Scope,
|
|
865
|
-
permissions: Permission[],
|
|
866
|
-
useCache: boolean = true
|
|
867
|
-
): {
|
|
868
|
-
hasAll: boolean;
|
|
869
|
-
isLoading: boolean;
|
|
870
|
-
error: Error | null;
|
|
871
|
-
refetch: () => Promise<void>;
|
|
872
|
-
} {
|
|
873
|
-
const [hasAll, setHasAll] = useState<boolean>(false);
|
|
874
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
875
|
-
const [error, setError] = useState<Error | null>(null);
|
|
876
|
-
|
|
877
|
-
const checkAllPermissions = useCallback(async () => {
|
|
878
|
-
if (!userId || permissions.length === 0) {
|
|
879
|
-
setHasAll(false);
|
|
880
|
-
setIsLoading(false);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
setIsLoading(true);
|
|
886
|
-
setError(null);
|
|
887
|
-
|
|
888
|
-
let hasAllPermissions = true;
|
|
889
|
-
|
|
890
|
-
for (const permission of permissions) {
|
|
891
|
-
const result = useCache
|
|
892
|
-
? await isPermittedCached({ userId, scope, permission })
|
|
893
|
-
: await isPermitted({ userId, scope, permission });
|
|
894
|
-
|
|
895
|
-
if (!result) {
|
|
896
|
-
hasAllPermissions = false;
|
|
897
|
-
break;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
setHasAll(hasAllPermissions);
|
|
902
|
-
} catch (err) {
|
|
903
|
-
setError(err instanceof Error ? err : new Error('Failed to check permissions'));
|
|
904
|
-
setHasAll(false);
|
|
905
|
-
} finally {
|
|
906
|
-
setIsLoading(false);
|
|
907
|
-
}
|
|
908
|
-
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
909
|
-
|
|
910
|
-
useEffect(() => {
|
|
911
|
-
checkAllPermissions();
|
|
912
|
-
}, [checkAllPermissions]);
|
|
913
|
-
|
|
914
|
-
// Memoize the return object to prevent unnecessary re-renders
|
|
915
|
-
return useMemo(() => ({
|
|
916
|
-
hasAll,
|
|
917
|
-
isLoading,
|
|
918
|
-
error,
|
|
919
|
-
refetch: checkAllPermissions
|
|
920
|
-
}), [hasAll, isLoading, error, checkAllPermissions]);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Hook to get cached permissions with TTL management
|
|
925
|
-
*
|
|
926
|
-
* @param userId - User ID
|
|
927
|
-
* @param scope - Scope for permission checking
|
|
928
|
-
* @returns Cached permission state and methods
|
|
929
|
-
*
|
|
930
|
-
* @example
|
|
931
|
-
* ```tsx
|
|
932
|
-
* function MyComponent() {
|
|
933
|
-
* const { permissions, isLoading, error, invalidateCache } = useCachedPermissions(userId, scope);
|
|
934
|
-
*
|
|
935
|
-
* if (isLoading) return <div>Loading cached permissions...</div>;
|
|
936
|
-
* if (error) return <div>Error: {error.message}</div>;
|
|
937
|
-
*
|
|
938
|
-
* return (
|
|
939
|
-
* <div>
|
|
940
|
-
* {permissions['read:users'] && <UserList />}
|
|
941
|
-
* <button onClick={invalidateCache}>Refresh Permissions</button>
|
|
942
|
-
* </div>
|
|
943
|
-
* );
|
|
944
|
-
* }
|
|
945
|
-
* ```
|
|
946
|
-
*/
|
|
947
|
-
export function useCachedPermissions(userId: UUID, scope: Scope): {
|
|
948
|
-
permissions: PermissionMap;
|
|
949
|
-
isLoading: boolean;
|
|
950
|
-
error: Error | null;
|
|
951
|
-
invalidateCache: () => void;
|
|
952
|
-
refetch: () => Promise<void>;
|
|
953
|
-
} {
|
|
954
|
-
const [permissions, setPermissions] = useState<PermissionMap>({} as PermissionMap);
|
|
955
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
956
|
-
const [error, setError] = useState<Error | null>(null);
|
|
957
|
-
|
|
958
|
-
const fetchCachedPermissions = useCallback(async () => {
|
|
959
|
-
if (!userId) {
|
|
960
|
-
setPermissions({} as PermissionMap);
|
|
961
|
-
setIsLoading(false);
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
try {
|
|
966
|
-
setIsLoading(true);
|
|
967
|
-
setError(null);
|
|
968
|
-
|
|
969
|
-
const permissionMap = await getPermissionMap({ userId, scope });
|
|
970
|
-
setPermissions(permissionMap);
|
|
971
|
-
} catch (err) {
|
|
972
|
-
setError(err instanceof Error ? err : new Error('Failed to fetch cached permissions'));
|
|
973
|
-
} finally {
|
|
974
|
-
setIsLoading(false);
|
|
975
|
-
}
|
|
976
|
-
}, [userId, scope.organisationId, scope.eventId, scope.appId]);
|
|
977
|
-
|
|
978
|
-
const invalidateCache = useCallback(() => {
|
|
979
|
-
// This would typically invalidate the cache in the actual implementation
|
|
980
|
-
// For now, we'll just refetch
|
|
981
|
-
fetchCachedPermissions();
|
|
982
|
-
}, [fetchCachedPermissions]);
|
|
983
|
-
|
|
984
|
-
useEffect(() => {
|
|
985
|
-
fetchCachedPermissions();
|
|
986
|
-
}, [fetchCachedPermissions]);
|
|
987
|
-
|
|
988
|
-
// Memoize the return object to prevent unnecessary re-renders
|
|
989
|
-
return useMemo(() => ({
|
|
990
|
-
permissions,
|
|
991
|
-
isLoading,
|
|
992
|
-
error,
|
|
993
|
-
invalidateCache,
|
|
994
|
-
refetch: fetchCachedPermissions
|
|
995
|
-
}), [permissions, isLoading, error, invalidateCache, fetchCachedPermissions]);
|
|
996
|
-
}
|
|
2
|
+
* RBAC Permission Hooks
|
|
3
|
+
*
|
|
4
|
+
* This barrel file re-exports the permission-related hooks. The implementations
|
|
5
|
+
* are organized in `./permissions` to keep the modules focused and maintainable.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
usePermissions,
|
|
9
|
+
useCan,
|
|
10
|
+
useAccessLevel,
|
|
11
|
+
useMultiplePermissions,
|
|
12
|
+
useHasAnyPermission,
|
|
13
|
+
useHasAllPermissions,
|
|
14
|
+
useCachedPermissions,
|
|
15
|
+
} from './permissions';
|