@jmruthers/pace-core 0.6.1 → 0.6.3
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 +88 -10
- package/cursor-rules/00-pace-core-compliance.mdc +46 -87
- package/cursor-rules/01-standards-compliance.mdc +16 -47
- package/cursor-rules/02-project-structure.mdc +4 -4
- package/cursor-rules/03-solid-principles.mdc +45 -164
- package/cursor-rules/04-testing-standards.mdc +22 -69
- package/cursor-rules/05-bug-reports-and-features.mdc +2 -2
- package/cursor-rules/06-code-quality.mdc +42 -125
- package/cursor-rules/07-tech-stack-compliance.mdc +33 -128
- package/cursor-rules/08-markup-quality.mdc +452 -0
- package/cursor-rules/CHANGELOG.md +18 -0
- package/cursor-rules/README.md +2 -1
- package/dist/{AuthService-DjnJHDtC.d.ts → AuthService-Cb34EQs3.d.ts} +63 -1
- package/dist/{DataTable-CH1U5Tpy.d.ts → DataTable-BMRU8a1j.d.ts} +33 -1
- package/dist/{DataTable-DQ7RSOHE.js → DataTable-THFPBKTP.js} +12 -10
- package/dist/{PublicPageProvider-ce4xlHYA.d.ts → PublicPageProvider-DEMpysFR.d.ts} +394 -171
- package/dist/{UnifiedAuthProvider-185Ih4dj.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +30 -8
- package/dist/{UnifiedAuthProvider-ATAP5UTR.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
- package/dist/{api-N774RPUA.js → api-IAGWF3ZG.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/{chunk-JBKQ3SAO.js → chunk-2T2IG7T7.js} +107 -57
- package/dist/chunk-2T2IG7T7.js.map +1 -0
- package/dist/{chunk-3QRJFVBR.js → chunk-6SOIHG6Z.js} +1 -1
- package/dist/chunk-6SOIHG6Z.js.map +1 -0
- package/dist/{chunk-3XTALGJF.js → chunk-6Z7LTB3D.js} +69 -240
- package/dist/chunk-6Z7LTB3D.js.map +1 -0
- package/dist/{chunk-4ZC4GX36.js → chunk-CNCQDFLN.js} +199 -46
- package/dist/chunk-CNCQDFLN.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-BYFSK72L.js → chunk-DWUBLJJM.js} +361 -187
- package/dist/chunk-DWUBLJJM.js.map +1 -0
- package/dist/{chunk-LXQLPRQ2.js → chunk-FFQEQTNW.js} +6 -8
- 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-4N5C5XZU.js → chunk-HFZBI76P.js} +4 -4
- package/dist/chunk-HFZBI76P.js.map +1 -0
- package/dist/{chunk-SQGMNID3.js → chunk-L4OXEN46.js} +4 -5
- package/dist/chunk-L4OXEN46.js.map +1 -0
- package/dist/{chunk-R77UEZ4E.js → chunk-M43Y4SSO.js} +1 -1
- package/dist/chunk-M43Y4SSO.js.map +1 -0
- package/dist/{chunk-I7PSE6JW.js → chunk-M7MPQISP.js} +3 -76
- package/dist/chunk-M7MPQISP.js.map +1 -0
- package/dist/chunk-PQBSKX33.js +7793 -0
- package/dist/chunk-PQBSKX33.js.map +1 -0
- package/dist/chunk-QRPVRXYT.js +226 -0
- package/dist/chunk-QRPVRXYT.js.map +1 -0
- package/dist/{chunk-KNC55RTG.js → chunk-RWEBCB47.js} +194 -416
- package/dist/chunk-RWEBCB47.js.map +1 -0
- package/dist/{chunk-XM25TVIE.js → chunk-YDQHOZNA.js} +843 -388
- package/dist/chunk-YDQHOZNA.js.map +1 -0
- package/dist/{chunk-GLK6VM3F.js → chunk-ZNIWI3UC.js} +739 -737
- package/dist/chunk-ZNIWI3UC.js.map +1 -0
- package/dist/components.d.ts +5 -5
- package/dist/components.js +18 -16
- package/dist/components.js.map +1 -1
- package/dist/contextValidator-3JNZKUTX.js +9 -0
- package/dist/contextValidator-3JNZKUTX.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/{functions-D_kgHktt.d.ts → functions-DHebl8-F.d.ts} +1 -1
- package/dist/hooks.d.ts +55 -122
- package/dist/hooks.js +10 -13
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +60 -13
- package/dist/index.js +30 -25
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +21 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +210 -139
- package/dist/rbac/index.js +17 -13
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.d.ts +1 -13
- package/dist/theming/runtime.js +2 -2
- 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/types.js +1 -1
- package/dist/{usePublicRouteParams-BJAlWfuJ.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +38 -17
- package/dist/utils.d.ts +4 -5
- package/dist/utils.js +17 -19
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +21 -17
- package/docs/api/modules.md +4191 -2967
- package/docs/architecture/database-schema-requirements.md +161 -0
- package/docs/components/context-selector.md +126 -0
- package/docs/core-concepts/rbac-system.md +3 -3
- package/docs/documentation-index.md +2 -4
- package/docs/getting-started/cursor-rules.md +2 -1
- package/docs/migration/DOCUMENTATION_STRUCTURE.md +441 -0
- package/docs/migration/MIGRATION_GUIDE.md +2 -24
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/migration/README.md +52 -6
- package/docs/migration/V0.5.190_TO_V0.6.1_MIGRATION.md +1153 -0
- package/docs/migration/database-changes-december-2025.md +3 -3
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- 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/rbac/secure-client-protection.md +330 -0
- package/docs/standards/README.md +1 -0
- package/package.json +4 -3
- 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} +784 -685
- 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 +985 -0
- package/scripts/audit/core/checks/documentation.cjs +268 -0
- package/scripts/audit/core/checks/environment.cjs +116 -0
- package/scripts/audit/core/checks/error-handling.cjs +340 -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 +61 -936
- 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/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__/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 +14 -0
- package/src/components/Button/Button.tsx +22 -0
- package/src/components/Calendar/Calendar.tsx +8 -2
- 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/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/DataTable.tsx +38 -4
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +5 -6
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +18 -4
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +2 -3
- package/src/components/DataTable/components/AccessDeniedPage.tsx +16 -25
- package/src/components/DataTable/components/ActionButtons.tsx +10 -7
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
- 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 +196 -554
- 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 +8 -0
- 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 +8 -0
- 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 +63 -851
- 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/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 +12 -0
- 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 +127 -33
- package/src/components/DataTable/hooks/useDataTableState.ts +35 -1
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +12 -0
- 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/Dialog/Dialog.tsx +31 -3
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +180 -1
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +45 -5
- package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +129 -0
- package/src/components/ErrorBoundary/index.ts +27 -2
- package/src/components/FileDisplay/FileDisplay.tsx +74 -28
- package/src/components/FileUpload/FileUpload.tsx +22 -2
- package/src/components/Footer/Footer.test.tsx +16 -16
- package/src/components/Footer/Footer.tsx +14 -11
- package/src/components/Form/Form.tsx +1 -0
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +59 -49
- package/src/components/Input/Input.test.tsx +2 -2
- package/src/components/Input/Input.tsx +8 -4
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +4 -4
- package/src/components/LoginForm/LoginForm.tsx +4 -0
- package/src/components/NavigationMenu/NavigationMenu.tsx +14 -513
- package/src/components/NavigationMenu/types.ts +56 -0
- package/src/components/NavigationMenu/useNavigationFiltering.ts +390 -0
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +13 -11
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +167 -44
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +3 -4
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +3 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +9 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +3 -9
- package/src/components/PublicLayout/PublicPageLayout.tsx +2 -5
- package/src/components/PublicLayout/PublicPageProvider.tsx +4 -0
- package/src/components/Select/Select.tsx +80 -434
- 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 +4 -5
- package/src/components/Switch/Switch.tsx +4 -4
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Toast/Toast.tsx +4 -0
- package/src/components/Tooltip/Tooltip.tsx +2 -2
- package/src/components/UserMenu/UserMenu.test.tsx +24 -11
- package/src/components/UserMenu/UserMenu.tsx +21 -18
- package/src/components/index.ts +7 -7
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/index.unit.test.ts +2 -5
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/index.ts +1 -2
- package/src/hooks/public/usePublicEvent.ts +4 -0
- package/src/hooks/public/usePublicEventLogo.ts +4 -0
- package/src/hooks/public/usePublicFileDisplay.ts +4 -0
- package/src/hooks/public/usePublicRouteParams.ts +4 -0
- 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/useAppConfig.ts +15 -30
- package/src/hooks/useDebounce.ts +9 -0
- package/src/hooks/useEventTheme.ts +6 -0
- package/src/hooks/useFileDisplay.ts +81 -50
- package/src/hooks/useFileReference.ts +25 -7
- package/src/hooks/useFileUrl.ts +11 -1
- package/src/hooks/useFocusManagement.ts +14 -0
- package/src/hooks/useFocusTrap.ts +3 -0
- package/src/hooks/useInactivityTracker.ts +3 -0
- 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 +7 -0
- 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 +1 -1
- package/src/index.ts +6 -6
- package/src/providers/__tests__/OrganisationProvider.test.tsx +92 -70
- package/src/providers/services/AuthServiceProvider.tsx +35 -7
- package/src/providers/services/EventServiceProvider.tsx +51 -5
- package/src/providers/services/InactivityServiceProvider.tsx +18 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +18 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +126 -134
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +29 -13
- package/src/rbac/README.md +1 -1
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +4 -5
- package/src/rbac/adapters.tsx +12 -3
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +246 -167
- package/src/rbac/components/NavigationProvider.tsx +4 -1
- package/src/rbac/components/PagePermissionGuard.tsx +185 -17
- package/src/rbac/components/RoleBasedRouter.tsx +5 -1
- package/src/rbac/components/SecureDataProvider.test.tsx +84 -49
- package/src/rbac/components/SecureDataProvider.tsx +20 -5
- 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/__tests__/useSecureSupabase.test.ts +32 -21
- 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 +377 -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 +64 -66
- package/src/rbac/hooks/usePermissions.ts +14 -995
- package/src/rbac/hooks/useRBAC.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.ts +36 -37
- package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
- package/src/rbac/hooks/useResolvedScope.ts +35 -40
- package/src/rbac/hooks/useResourcePermissions.test.ts +54 -18
- package/src/rbac/hooks/useResourcePermissions.ts +14 -4
- package/src/rbac/hooks/useSecureSupabase.ts +27 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/permissions.ts +0 -30
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +294 -68
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +9 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
- package/src/rbac/utils/clientSecurity.ts +93 -0
- package/src/rbac/utils/contextValidator.ts +77 -168
- package/src/services/AuthService.ts +39 -7
- package/src/services/EventService.ts +186 -54
- package/src/services/OrganisationService.ts +81 -14
- package/src/services/__tests__/EventService.test.ts +1 -2
- package/src/services/base/BaseService.ts +3 -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/dynamic/dynamicUtils.ts +7 -4
- 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/dist/chunk-3QRJFVBR.js.map +0 -1
- package/dist/chunk-3XTALGJF.js.map +0 -1
- package/dist/chunk-4N5C5XZU.js.map +0 -1
- package/dist/chunk-4ZC4GX36.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-BYFSK72L.js.map +0 -1
- package/dist/chunk-EXUD6RNJ.js +0 -451
- package/dist/chunk-EXUD6RNJ.js.map +0 -1
- package/dist/chunk-GLK6VM3F.js.map +0 -1
- package/dist/chunk-I7PSE6JW.js.map +0 -1
- package/dist/chunk-JBKQ3SAO.js.map +0 -1
- package/dist/chunk-KNC55RTG.js.map +0 -1
- package/dist/chunk-LXQLPRQ2.js.map +0 -1
- package/dist/chunk-R77UEZ4E.js.map +0 -1
- package/dist/chunk-SQGMNID3.js.map +0 -1
- package/dist/chunk-T33XF5ZC.js +0 -12922
- package/dist/chunk-T33XF5ZC.js.map +0 -1
- package/dist/chunk-XM25TVIE.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/ErrorBoundary.md +0 -144
- package/docs/api/classes/InvalidScopeError.md +0 -73
- package/docs/api/classes/Logger.md +0 -178
- package/docs/api/classes/MissingUserContextError.md +0 -66
- package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
- package/docs/api/classes/PermissionDeniedError.md +0 -73
- package/docs/api/classes/RBACAuditManager.md +0 -297
- package/docs/api/classes/RBACCache.md +0 -322
- package/docs/api/classes/RBACEngine.md +0 -171
- package/docs/api/classes/RBACError.md +0 -76
- package/docs/api/classes/RBACNotInitializedError.md +0 -66
- package/docs/api/classes/SecureSupabaseClient.md +0 -160
- package/docs/api/classes/StorageUtils.md +0 -328
- package/docs/api/enums/FileCategory.md +0 -184
- package/docs/api/enums/LogLevel.md +0 -54
- package/docs/api/enums/RBACErrorCode.md +0 -228
- package/docs/api/enums/RPCFunction.md +0 -118
- package/docs/api/interfaces/AddressFieldProps.md +0 -241
- package/docs/api/interfaces/AddressFieldRef.md +0 -94
- package/docs/api/interfaces/AggregateConfig.md +0 -43
- package/docs/api/interfaces/AutocompleteOptions.md +0 -75
- package/docs/api/interfaces/AvatarProps.md +0 -128
- package/docs/api/interfaces/BadgeProps.md +0 -27
- package/docs/api/interfaces/ButtonProps.md +0 -53
- package/docs/api/interfaces/CalendarProps.md +0 -70
- package/docs/api/interfaces/CardProps.md +0 -66
- package/docs/api/interfaces/ColorPalette.md +0 -7
- package/docs/api/interfaces/ColorShade.md +0 -66
- package/docs/api/interfaces/ComplianceResult.md +0 -30
- package/docs/api/interfaces/DataAccessRecord.md +0 -96
- package/docs/api/interfaces/DataRecord.md +0 -11
- package/docs/api/interfaces/DataTableAction.md +0 -249
- package/docs/api/interfaces/DataTableColumn.md +0 -504
- package/docs/api/interfaces/DataTableProps.md +0 -625
- package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
- package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
- package/docs/api/interfaces/DatabaseIssue.md +0 -41
- package/docs/api/interfaces/EmptyStateConfig.md +0 -61
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
- package/docs/api/interfaces/EventAppRoleData.md +0 -71
- package/docs/api/interfaces/ExportColumn.md +0 -90
- package/docs/api/interfaces/ExportOptions.md +0 -126
- package/docs/api/interfaces/FileDisplayProps.md +0 -249
- package/docs/api/interfaces/FileMetadata.md +0 -129
- package/docs/api/interfaces/FileReference.md +0 -118
- package/docs/api/interfaces/FileSizeLimits.md +0 -7
- package/docs/api/interfaces/FileUploadOptions.md +0 -139
- package/docs/api/interfaces/FileUploadProps.md +0 -293
- package/docs/api/interfaces/FooterProps.md +0 -105
- package/docs/api/interfaces/FormFieldProps.md +0 -166
- package/docs/api/interfaces/FormProps.md +0 -113
- package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
- package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
- package/docs/api/interfaces/InputProps.md +0 -53
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -184
- package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
- package/docs/api/interfaces/NavigationContextType.md +0 -164
- package/docs/api/interfaces/NavigationGuardProps.md +0 -139
- package/docs/api/interfaces/NavigationItem.md +0 -120
- package/docs/api/interfaces/NavigationMenuProps.md +0 -221
- package/docs/api/interfaces/NavigationProviderProps.md +0 -117
- package/docs/api/interfaces/Organisation.md +0 -140
- package/docs/api/interfaces/OrganisationContextType.md +0 -388
- package/docs/api/interfaces/OrganisationMembership.md +0 -140
- package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
- package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
- package/docs/api/interfaces/PaceAppLayoutProps.md +0 -406
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -47
- package/docs/api/interfaces/PageAccessRecord.md +0 -85
- package/docs/api/interfaces/PagePermissionContextType.md +0 -140
- package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
- package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
- package/docs/api/interfaces/PaletteData.md +0 -41
- package/docs/api/interfaces/ParsedAddress.md +0 -120
- package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
- package/docs/api/interfaces/ProgressProps.md +0 -42
- package/docs/api/interfaces/ProtectedRouteProps.md +0 -97
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -198
- package/docs/api/interfaces/QuickFix.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
- package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
- package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
- package/docs/api/interfaces/RBACConfig.md +0 -133
- package/docs/api/interfaces/RBACContext.md +0 -52
- package/docs/api/interfaces/RBACLogger.md +0 -112
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
- package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
- package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
- package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
- package/docs/api/interfaces/RBACResult.md +0 -58
- package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
- package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
- package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
- package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
- package/docs/api/interfaces/RBACRolesListParams.md +0 -52
- package/docs/api/interfaces/RBACRolesListResult.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
- package/docs/api/interfaces/ResourcePermissions.md +0 -155
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
- package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
- package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
- package/docs/api/interfaces/RoleManagementResult.md +0 -52
- package/docs/api/interfaces/RouteAccessRecord.md +0 -107
- package/docs/api/interfaces/RouteConfig.md +0 -134
- package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
- package/docs/api/interfaces/SecureDataContextType.md +0 -168
- package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
- package/docs/api/interfaces/SetupIssue.md +0 -41
- package/docs/api/interfaces/StorageConfig.md +0 -41
- package/docs/api/interfaces/StorageFileInfo.md +0 -74
- package/docs/api/interfaces/StorageFileMetadata.md +0 -151
- package/docs/api/interfaces/StorageListOptions.md +0 -99
- package/docs/api/interfaces/StorageListResult.md +0 -41
- package/docs/api/interfaces/StorageUploadOptions.md +0 -101
- package/docs/api/interfaces/StorageUploadResult.md +0 -63
- package/docs/api/interfaces/StorageUrlOptions.md +0 -60
- package/docs/api/interfaces/StyleImport.md +0 -19
- package/docs/api/interfaces/SwitchProps.md +0 -34
- package/docs/api/interfaces/TabsContentProps.md +0 -9
- package/docs/api/interfaces/TabsListProps.md +0 -9
- package/docs/api/interfaces/TabsProps.md +0 -9
- package/docs/api/interfaces/TabsTriggerProps.md +0 -50
- package/docs/api/interfaces/TextareaProps.md +0 -53
- package/docs/api/interfaces/ToastActionElement.md +0 -9
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -820
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -171
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -136
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -81
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -68
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -120
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -94
- package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
- package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
- package/docs/api/interfaces/UserEventAccess.md +0 -118
- package/docs/api/interfaces/UserMenuProps.md +0 -86
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/docs/migration/quick-migration-guide.md +0 -356
- package/docs/migration/service-architecture.md +0 -281
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -420
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -324
- package/src/components/OrganisationSelector/index.ts +0 -9
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +0 -680
- package/src/hooks/useSecureDataAccess.test.ts +0 -559
- package/src/hooks/useSecureDataAccess.ts +0 -681
- /package/dist/{DataTable-DQ7RSOHE.js.map → DataTable-THFPBKTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-ATAP5UTR.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
- /package/dist/{api-N774RPUA.js.map → api-IAGWF3ZG.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.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/docs/migration/{REACT_19_MIGRATION.md → V0.6.0_REACT_19_MIGRATION.md} +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
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
|
+
// CRITICAL FIX: Initialize isSuperAdmin from precomputed value immediately
|
|
47
|
+
// This prevents permission checks from running when we already know the user is a super admin
|
|
48
|
+
// If precomputedSuperAdmin is true, we can immediately set can=true and isLoading=false
|
|
49
|
+
const [isSuperAdmin, setIsSuperAdmin] = useState<boolean | null>(precomputedSuperAdmin ?? null);
|
|
50
|
+
|
|
51
|
+
// For super admins, immediately grant permissions without waiting
|
|
52
|
+
const initialCan = precomputedSuperAdmin === true ? true : false;
|
|
53
|
+
const initialIsLoading = precomputedSuperAdmin === true ? false : true;
|
|
54
|
+
|
|
55
|
+
const [can, setCan] = useState<boolean>(initialCan);
|
|
56
|
+
const [isLoading, setIsLoading] = useState<boolean>(initialIsLoading);
|
|
57
|
+
const [error, setError] = useState<Error | null>(null);
|
|
58
|
+
|
|
59
|
+
// Validate scope parameter - handle undefined/null scope gracefully
|
|
60
|
+
const isValidScope = scope && typeof scope === 'object';
|
|
61
|
+
const organisationId = isValidScope ? scope.organisationId : undefined;
|
|
62
|
+
const eventId = isValidScope ? scope.eventId : undefined;
|
|
63
|
+
const appId = isValidScope ? scope.appId : undefined;
|
|
64
|
+
|
|
65
|
+
// CRITICAL FIX: Immediately update state when precomputedSuperAdmin changes to true
|
|
66
|
+
// This ensures super admins get immediate access without waiting for permission checks
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (precomputedSuperAdmin === true && isSuperAdmin !== true) {
|
|
69
|
+
setIsSuperAdmin(true);
|
|
70
|
+
setCan(true);
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
setError(null);
|
|
73
|
+
} else if (precomputedSuperAdmin === false && isSuperAdmin !== false) {
|
|
74
|
+
setIsSuperAdmin(false);
|
|
75
|
+
}
|
|
76
|
+
}, [precomputedSuperAdmin, isSuperAdmin]);
|
|
77
|
+
|
|
78
|
+
// Check super-admin status - super admins bypass organisation context requirements
|
|
79
|
+
// PERFORMANCE OPTIMIZATION: Use precomputed value directly - no duplicate checks
|
|
80
|
+
// Callers must check super admin once and pass the result (null if not checked yet)
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
// If precomputed value is null, it means not checked yet - check ourselves
|
|
83
|
+
if (precomputedSuperAdmin === null) {
|
|
84
|
+
if (!userId) {
|
|
85
|
+
setIsSuperAdmin(false);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
const checkSuperAdmin = async () => {
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
try {
|
|
93
|
+
const { isSuperAdmin: checkSuperAdmin } = await import('../../api');
|
|
94
|
+
|
|
95
|
+
// Add timeout warning
|
|
96
|
+
const timeoutWarning = setTimeout(() => {
|
|
97
|
+
if (!cancelled) {
|
|
98
|
+
console.warn('[useCan] Super admin check taking longer than 5 seconds', {
|
|
99
|
+
userId,
|
|
100
|
+
elapsedMs: Date.now() - startTime,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}, 5000);
|
|
104
|
+
|
|
105
|
+
const isSuper = await checkSuperAdmin(userId);
|
|
106
|
+
clearTimeout(timeoutWarning);
|
|
107
|
+
|
|
108
|
+
if (!cancelled) {
|
|
109
|
+
const elapsed = Date.now() - startTime;
|
|
110
|
+
if (elapsed > 1000) {
|
|
111
|
+
console.warn('[useCan] Super admin check took longer than expected', {
|
|
112
|
+
userId,
|
|
113
|
+
elapsedMs: elapsed,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
setIsSuperAdmin(isSuper);
|
|
117
|
+
// If super admin, immediately grant permissions
|
|
118
|
+
if (isSuper) {
|
|
119
|
+
setCan(true);
|
|
120
|
+
setIsLoading(false);
|
|
121
|
+
setError(null);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (!cancelled) {
|
|
126
|
+
const elapsed = Date.now() - startTime;
|
|
127
|
+
console.error('[useCan] Error checking super admin', {
|
|
128
|
+
userId,
|
|
129
|
+
error: err,
|
|
130
|
+
elapsedMs: elapsed,
|
|
131
|
+
});
|
|
132
|
+
setIsSuperAdmin(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
checkSuperAdmin();
|
|
138
|
+
return () => {
|
|
139
|
+
cancelled = true;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}, [userId, precomputedSuperAdmin]);
|
|
143
|
+
|
|
144
|
+
// Add timeout for missing organisation context (3 seconds)
|
|
145
|
+
// Only apply timeout for resource-level permissions, not page-level (which can handle null orgs)
|
|
146
|
+
// Super admins bypass this check
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
149
|
+
const requiresOrgId = !isPagePermission;
|
|
150
|
+
|
|
151
|
+
// Don't block if user is super-admin (they bypass context requirements)
|
|
152
|
+
if (isSuperAdmin === true) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (requiresOrgId && (!isValidScope || !organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
157
|
+
const timeoutId = setTimeout(() => {
|
|
158
|
+
setError(new Error('Organisation context is required for permission checks'));
|
|
159
|
+
setIsLoading(false);
|
|
160
|
+
setCan(false);
|
|
161
|
+
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
162
|
+
|
|
163
|
+
return () => clearTimeout(timeoutId);
|
|
164
|
+
}
|
|
165
|
+
// Clear error if organisation context becomes available
|
|
166
|
+
if (error?.message === 'Organisation context is required for permission checks') {
|
|
167
|
+
setError(null);
|
|
168
|
+
}
|
|
169
|
+
}, [isValidScope, organisationId, error, permission, pageId, isSuperAdmin]);
|
|
170
|
+
|
|
171
|
+
// Use refs to track the last values to prevent unnecessary re-runs
|
|
172
|
+
const lastUserIdRef = useRef<UUID | null>(null);
|
|
173
|
+
const lastScopeRef = useRef<string | null>(null);
|
|
174
|
+
const lastPermissionRef = useRef<Permission | null>(null);
|
|
175
|
+
const lastPageIdRef = useRef<UUID | undefined | null>(null);
|
|
176
|
+
const lastUseCacheRef = useRef<boolean | null>(null);
|
|
177
|
+
const lastIsSuperAdminRef = useRef<boolean | null>(null);
|
|
178
|
+
|
|
179
|
+
// Create a stable scope object for comparison
|
|
180
|
+
const stableScope = useMemo(() => {
|
|
181
|
+
if (!isValidScope) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
organisationId,
|
|
186
|
+
eventId,
|
|
187
|
+
appId,
|
|
188
|
+
};
|
|
189
|
+
}, [isValidScope, organisationId, eventId, appId]);
|
|
190
|
+
|
|
191
|
+
// Track previous scope for deep equality comparison
|
|
192
|
+
const prevScopeRef = useRef<Scope | null>(null);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
// Use deep equality check for scope to prevent unnecessary re-runs
|
|
196
|
+
const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
|
|
197
|
+
|
|
198
|
+
// Only run if something has actually changed
|
|
199
|
+
// CRITICAL: Also check if isSuperAdmin changed - super admins bypass all checks
|
|
200
|
+
const isSuperAdminChanged = lastIsSuperAdminRef.current !== isSuperAdmin;
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
lastUserIdRef.current !== userId ||
|
|
204
|
+
scopeChanged ||
|
|
205
|
+
lastPermissionRef.current !== permission ||
|
|
206
|
+
lastPageIdRef.current !== pageId ||
|
|
207
|
+
lastUseCacheRef.current !== useCache ||
|
|
208
|
+
isSuperAdminChanged
|
|
209
|
+
) {
|
|
210
|
+
lastIsSuperAdminRef.current = isSuperAdmin;
|
|
211
|
+
lastUserIdRef.current = userId;
|
|
212
|
+
prevScopeRef.current = stableScope;
|
|
213
|
+
lastPermissionRef.current = permission;
|
|
214
|
+
lastPageIdRef.current = pageId;
|
|
215
|
+
lastUseCacheRef.current = useCache;
|
|
216
|
+
|
|
217
|
+
// Inline the permission check logic to avoid useCallback dependency issues
|
|
218
|
+
const checkPermission = async () => {
|
|
219
|
+
if (!userId) {
|
|
220
|
+
setCan(false);
|
|
221
|
+
setIsLoading(false);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// CRITICAL: Super admins bypass all permission checks - grant immediately
|
|
226
|
+
// This must be checked BEFORE any other validation to avoid unnecessary API calls
|
|
227
|
+
if (isSuperAdmin === true) {
|
|
228
|
+
setCan(true);
|
|
229
|
+
setIsLoading(false);
|
|
230
|
+
setError(null);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If super admin status is still being checked (null), wait for it to complete
|
|
235
|
+
// Don't proceed with permission check until we know if user is super admin
|
|
236
|
+
if (isSuperAdmin === null) {
|
|
237
|
+
setIsLoading(true);
|
|
238
|
+
setCan(false);
|
|
239
|
+
setError(null);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Validate scope before accessing properties
|
|
244
|
+
if (!isValidScope) {
|
|
245
|
+
setIsLoading(true);
|
|
246
|
+
setCan(false);
|
|
247
|
+
setError(null);
|
|
248
|
+
// Timeout is handled in separate useEffect
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// For page-level permissions, allow undefined/null organisationId (database function handles it)
|
|
253
|
+
// For resource-level permissions, organisationId is required
|
|
254
|
+
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
255
|
+
const requiresOrgId = !isPagePermission;
|
|
256
|
+
|
|
257
|
+
// Check if pageId is a pageName (not a UUID) - if so, we need appId to resolve it
|
|
258
|
+
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);
|
|
259
|
+
const needsAppIdForPageName = isPagePermission && isPageName;
|
|
260
|
+
|
|
261
|
+
// Don't check permissions if scope is invalid and orgId is required
|
|
262
|
+
// Wait for organisation context to resolve (unless it's a page permission that can handle null orgs)
|
|
263
|
+
if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
264
|
+
// Not super-admin (already checked above) - wait for org context
|
|
265
|
+
setIsLoading(true);
|
|
266
|
+
setCan(false);
|
|
267
|
+
setError(null);
|
|
268
|
+
// Timeout is handled in separate useEffect (Phase 1.4)
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// For page-level permissions with pageName (not UUID), we need appId to resolve the pageName to pageId
|
|
273
|
+
// Wait for appId to be available before checking permissions
|
|
274
|
+
if (needsAppIdForPageName && (!appId || appId === null || (typeof appId === 'string' && appId.trim() === ''))) {
|
|
275
|
+
setIsLoading(true);
|
|
276
|
+
setCan(false);
|
|
277
|
+
setError(null);
|
|
278
|
+
// Will re-run when appId becomes available (via scope change detection)
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
setIsLoading(true);
|
|
284
|
+
setError(null);
|
|
285
|
+
|
|
286
|
+
// Create a valid scope object for the API call
|
|
287
|
+
// For page-level permissions, organisationId can be undefined (database handles it)
|
|
288
|
+
const validScope: Scope = {
|
|
289
|
+
...(organisationId ? { organisationId } : {}),
|
|
290
|
+
...(eventId ? { eventId } : {}),
|
|
291
|
+
...(appId ? { appId } : {})
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Pass super admin status to avoid duplicate check in isPermitted
|
|
295
|
+
// Note: isPermittedCached doesn't support precomputedSuperAdmin, but the check will be cached
|
|
296
|
+
// If we know user is NOT super admin (isSuperAdmin === false), pass false to skip the check
|
|
297
|
+
const result = useCache
|
|
298
|
+
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
|
|
299
|
+
: await isPermitted({ userId, scope: validScope, permission, pageId }, appName, isSuperAdmin === false ? false : null);
|
|
300
|
+
|
|
301
|
+
setCan(result);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
const logger = getRBACLogger();
|
|
304
|
+
logger.error('Permission check error:', { permission, error: err });
|
|
305
|
+
console.error('[useCan] Permission check error', { userId, permission, error: err });
|
|
306
|
+
setError(err instanceof Error ? err : new Error('Failed to check permission'));
|
|
307
|
+
setCan(false);
|
|
308
|
+
} finally {
|
|
309
|
+
setIsLoading(false);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
checkPermission();
|
|
314
|
+
}
|
|
315
|
+
}, [userId, stableScope, permission, pageId, useCache, appName, isSuperAdmin]);
|
|
316
|
+
|
|
317
|
+
const refetch = useCallback(async () => {
|
|
318
|
+
if (!userId) {
|
|
319
|
+
setCan(false);
|
|
320
|
+
setIsLoading(false);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Validate scope before accessing properties
|
|
325
|
+
if (!isValidScope) {
|
|
326
|
+
setCan(false);
|
|
327
|
+
setIsLoading(true);
|
|
328
|
+
setError(null);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// For page-level permissions, allow undefined/null organisationId (database function handles it)
|
|
333
|
+
// For resource-level permissions, organisationId is required
|
|
334
|
+
const isPagePermission = permission.includes(':page.') || !!pageId;
|
|
335
|
+
const requiresOrgId = !isPagePermission;
|
|
336
|
+
|
|
337
|
+
// Don't check permissions if scope is invalid and orgId is required
|
|
338
|
+
if (requiresOrgId && (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === ''))) {
|
|
339
|
+
setCan(false);
|
|
340
|
+
setIsLoading(true);
|
|
341
|
+
setError(null);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
setIsLoading(true);
|
|
347
|
+
setError(null);
|
|
348
|
+
|
|
349
|
+
// Create a valid scope object for the API call
|
|
350
|
+
// For page-level permissions, organisationId can be undefined (database handles it)
|
|
351
|
+
const validScope: Scope = {
|
|
352
|
+
...(organisationId ? { organisationId } : {}),
|
|
353
|
+
...(eventId ? { eventId } : {}),
|
|
354
|
+
...(appId ? { appId } : {})
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const result = useCache
|
|
358
|
+
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
|
|
359
|
+
: await isPermitted({ userId, scope: validScope, permission, pageId }, appName, null);
|
|
360
|
+
|
|
361
|
+
setCan(result);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
setError(err instanceof Error ? err : new Error('Failed to check permission'));
|
|
364
|
+
setCan(false);
|
|
365
|
+
} finally {
|
|
366
|
+
setIsLoading(false);
|
|
367
|
+
}
|
|
368
|
+
}, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache, appName]);
|
|
369
|
+
|
|
370
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
371
|
+
return useMemo(() => ({
|
|
372
|
+
can,
|
|
373
|
+
isLoading,
|
|
374
|
+
error,
|
|
375
|
+
refetch
|
|
376
|
+
}), [can, isLoading, error, refetch]);
|
|
377
|
+
}
|
|
@@ -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 });
|
|
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
|
+
}
|
|
@@ -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 any 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 any of the permissions
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function MyComponent() {
|
|
18
|
+
* const { hasAny, isLoading, error } = useHasAnyPermission(
|
|
19
|
+
* userId,
|
|
20
|
+
* scope,
|
|
21
|
+
* ['read:users', 'create:users']
|
|
22
|
+
* );
|
|
23
|
+
*
|
|
24
|
+
* if (isLoading) return <div>Checking permissions...</div>;
|
|
25
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
26
|
+
*
|
|
27
|
+
* return hasAny ? <UserManagementPanel /> : <div>No user permissions</div>;
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useHasAnyPermission(
|
|
32
|
+
userId: UUID,
|
|
33
|
+
scope: Scope,
|
|
34
|
+
permissions: Permission[],
|
|
35
|
+
useCache: boolean = true
|
|
36
|
+
): {
|
|
37
|
+
hasAny: boolean;
|
|
38
|
+
isLoading: boolean;
|
|
39
|
+
error: Error | null;
|
|
40
|
+
refetch: () => Promise<void>;
|
|
41
|
+
} {
|
|
42
|
+
const [hasAny, setHasAny] = useState<boolean>(false);
|
|
43
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
44
|
+
const [error, setError] = useState<Error | null>(null);
|
|
45
|
+
|
|
46
|
+
const checkAnyPermission = useCallback(async () => {
|
|
47
|
+
if (!userId || permissions.length === 0) {
|
|
48
|
+
setHasAny(false);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
|
|
57
|
+
let hasAnyPermission = false;
|
|
58
|
+
|
|
59
|
+
for (const permission of permissions) {
|
|
60
|
+
const result = useCache
|
|
61
|
+
? await isPermittedCached({ userId, scope, permission })
|
|
62
|
+
: await isPermitted({ userId, scope, permission });
|
|
63
|
+
|
|
64
|
+
if (result) {
|
|
65
|
+
hasAnyPermission = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setHasAny(hasAnyPermission);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
setError(err instanceof Error ? err : new Error('Failed to check permissions'));
|
|
73
|
+
setHasAny(false);
|
|
74
|
+
} finally {
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
checkAnyPermission();
|
|
81
|
+
}, [checkAnyPermission]);
|
|
82
|
+
|
|
83
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
84
|
+
return useMemo(() => ({
|
|
85
|
+
hasAny,
|
|
86
|
+
isLoading,
|
|
87
|
+
error,
|
|
88
|
+
refetch: checkAnyPermission
|
|
89
|
+
}), [hasAny, isLoading, error, checkAnyPermission]);
|
|
90
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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 multiple permissions at once
|
|
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 Multiple permission check results
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function MyComponent() {
|
|
18
|
+
* const { results, isLoading, error } = useMultiplePermissions(
|
|
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 (
|
|
28
|
+
* <div>
|
|
29
|
+
* {results['read:users'] && <UserList />}
|
|
30
|
+
* {results['create:users'] && <CreateUserButton />}
|
|
31
|
+
* {results['update:users'] && <EditUserButton />}
|
|
32
|
+
* </div>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function useMultiplePermissions(
|
|
38
|
+
userId: UUID,
|
|
39
|
+
scope: Scope,
|
|
40
|
+
permissions: Permission[],
|
|
41
|
+
useCache: boolean = true
|
|
42
|
+
): {
|
|
43
|
+
results: Record<Permission, boolean>;
|
|
44
|
+
isLoading: boolean;
|
|
45
|
+
error: Error | null;
|
|
46
|
+
refetch: () => Promise<void>;
|
|
47
|
+
} {
|
|
48
|
+
const [results, setResults] = useState<Record<Permission, boolean>>({} as Record<Permission, boolean>);
|
|
49
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
50
|
+
const [error, setError] = useState<Error | null>(null);
|
|
51
|
+
|
|
52
|
+
const checkPermissions = useCallback(async () => {
|
|
53
|
+
if (!userId || permissions.length === 0) {
|
|
54
|
+
setResults({} as Record<Permission, boolean>);
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
setIsLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
|
|
63
|
+
const permissionResults: Record<Permission, boolean> = {} as Record<Permission, boolean>;
|
|
64
|
+
|
|
65
|
+
// Check each permission
|
|
66
|
+
for (const permission of permissions) {
|
|
67
|
+
const result = useCache
|
|
68
|
+
? await isPermittedCached({ userId, scope, permission })
|
|
69
|
+
: await isPermitted({ userId, scope, permission });
|
|
70
|
+
permissionResults[permission] = result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setResults(permissionResults);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
setError(err instanceof Error ? err : new Error('Failed to check permissions'));
|
|
76
|
+
setResults({} as Record<Permission, boolean>);
|
|
77
|
+
} finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
checkPermissions();
|
|
84
|
+
}, [checkPermissions]);
|
|
85
|
+
|
|
86
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
87
|
+
return useMemo(() => ({
|
|
88
|
+
results,
|
|
89
|
+
isLoading,
|
|
90
|
+
error,
|
|
91
|
+
refetch: checkPermissions
|
|
92
|
+
}), [results, isLoading, error, checkPermissions]);
|
|
93
|
+
}
|