@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
|
@@ -1,50 +1,96 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useAppConfig,
|
|
3
3
|
useEvents,
|
|
4
|
-
useOrganisationSecurity
|
|
5
|
-
|
|
6
|
-
} from "./chunk-3XTALGJF.js";
|
|
4
|
+
useOrganisationSecurity
|
|
5
|
+
} from "./chunk-6Z7LTB3D.js";
|
|
7
6
|
import {
|
|
8
7
|
useOrganisations,
|
|
9
8
|
useUnifiedAuth
|
|
10
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-DWUBLJJM.js";
|
|
11
10
|
import {
|
|
12
|
-
ContextValidator,
|
|
13
|
-
OrganisationContextRequiredError,
|
|
14
11
|
getAccessLevel,
|
|
12
|
+
getPageScopeType,
|
|
15
13
|
getPermissionMap,
|
|
16
14
|
getRBACLogger,
|
|
17
15
|
getRoleContext,
|
|
18
16
|
isPermitted,
|
|
19
17
|
isPermittedCached,
|
|
20
18
|
resolveAppContext
|
|
21
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-RWEBCB47.js";
|
|
20
|
+
import {
|
|
21
|
+
ContextValidator,
|
|
22
|
+
OrganisationContextRequiredError
|
|
23
|
+
} from "./chunk-QRPVRXYT.js";
|
|
22
24
|
import {
|
|
25
|
+
getCurrentAppName
|
|
26
|
+
} from "./chunk-M7MPQISP.js";
|
|
27
|
+
import {
|
|
28
|
+
createLogger,
|
|
23
29
|
logger
|
|
24
30
|
} from "./chunk-PWLANIRT.js";
|
|
25
31
|
|
|
32
|
+
// src/rbac/utils/clientSecurity.ts
|
|
33
|
+
var SECURE_CLIENT_SYMBOL = Symbol("pace-core-secure-client");
|
|
34
|
+
function isSecureClient(client) {
|
|
35
|
+
if (!client) return false;
|
|
36
|
+
return client[SECURE_CLIENT_SYMBOL] === true;
|
|
37
|
+
}
|
|
38
|
+
function warnIfInsecureClient(client, context) {
|
|
39
|
+
if (typeof process !== "undefined" && true) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!client) return;
|
|
43
|
+
if (!isSecureClient(client)) {
|
|
44
|
+
const contextMsg = context ? ` in ${context}` : "";
|
|
45
|
+
console.warn(
|
|
46
|
+
`[pace-core Security Warning] Non-secure Supabase client detected${contextMsg}.
|
|
47
|
+
You are using a Supabase client created with createClient() instead of useSecureSupabase().
|
|
48
|
+
This bypasses organisation context enforcement and RLS policies, which can lead to:
|
|
49
|
+
- Cross-organisation data access
|
|
50
|
+
- Security vulnerabilities
|
|
51
|
+
- Data leakage between organisations
|
|
52
|
+
|
|
53
|
+
Fix: Replace with:
|
|
54
|
+
import { useSecureSupabase } from '@jmruthers/pace-core/rbac';
|
|
55
|
+
const supabase = useSecureSupabase();
|
|
56
|
+
|
|
57
|
+
See: https://github.com/jmruthers/pace-core/blob/main/packages/core/docs/rbac/getting-started.md`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function markClientAsSecure(client) {
|
|
62
|
+
client[SECURE_CLIENT_SYMBOL] = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
// src/rbac/secureClient.ts
|
|
27
66
|
import { createClient } from "@supabase/supabase-js";
|
|
28
|
-
var
|
|
29
|
-
constructor(supabaseUrl, supabaseKey, organisationId, eventId, appId, isSuperAdmin = false) {
|
|
67
|
+
var _SecureSupabaseClient = class _SecureSupabaseClient {
|
|
68
|
+
constructor(supabaseUrl, supabaseKey, organisationId, eventId, appId, isSuperAdmin = false, existingClient) {
|
|
30
69
|
this.edgeFunctionClient = null;
|
|
70
|
+
this.usesExistingClient = false;
|
|
31
71
|
this.supabaseUrl = supabaseUrl;
|
|
32
72
|
this.supabaseKey = supabaseKey;
|
|
33
73
|
this.organisationId = organisationId;
|
|
34
74
|
this.eventId = eventId;
|
|
35
75
|
this.appId = appId;
|
|
36
76
|
this.isSuperAdmin = isSuperAdmin;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
77
|
+
if (existingClient) {
|
|
78
|
+
this.supabase = existingClient;
|
|
79
|
+
this.usesExistingClient = true;
|
|
80
|
+
} else {
|
|
81
|
+
this.supabase = createClient(supabaseUrl, supabaseKey, {
|
|
82
|
+
global: {
|
|
83
|
+
headers: {
|
|
84
|
+
"x-organisation-id": organisationId || "",
|
|
85
|
+
"x-event-id": eventId || "",
|
|
86
|
+
"x-app-id": appId || ""
|
|
87
|
+
}
|
|
43
88
|
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
46
91
|
this.setupContextInjection();
|
|
47
92
|
this.setupEdgeFunctionHandling();
|
|
93
|
+
markClientAsSecure(this.supabase);
|
|
48
94
|
}
|
|
49
95
|
/**
|
|
50
96
|
* Setup context injection for all database operations
|
|
@@ -52,20 +98,26 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
52
98
|
setupContextInjection() {
|
|
53
99
|
const originalFrom = this.supabase.from.bind(this.supabase);
|
|
54
100
|
this.supabase.from = (table) => {
|
|
55
|
-
this.
|
|
101
|
+
this.validateContextForTable(table);
|
|
56
102
|
const query = originalFrom(table);
|
|
57
103
|
query._tableName = table;
|
|
58
104
|
return this.injectContext(query, table);
|
|
59
105
|
};
|
|
60
106
|
const originalRpc = this.supabase.rpc.bind(this.supabase);
|
|
61
107
|
this.supabase.rpc = (fn, args, options) => {
|
|
62
|
-
this.
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
108
|
+
this.validateContextForRpc(fn);
|
|
109
|
+
const acceptedParams = this.getRpcAcceptedParams(fn);
|
|
110
|
+
const safeArgs = args ?? {};
|
|
111
|
+
const contextArgs = { ...safeArgs };
|
|
112
|
+
if (acceptedParams.has("p_organisation_id") && this.organisationId && safeArgs.p_organisation_id === void 0) {
|
|
113
|
+
contextArgs.p_organisation_id = this.organisationId;
|
|
114
|
+
}
|
|
115
|
+
if (acceptedParams.has("p_event_id") && this.eventId && safeArgs.p_event_id === void 0) {
|
|
116
|
+
contextArgs.p_event_id = this.eventId;
|
|
117
|
+
}
|
|
118
|
+
if (acceptedParams.has("p_app_id") && this.appId && safeArgs.p_app_id === void 0) {
|
|
119
|
+
contextArgs.p_app_id = this.appId;
|
|
120
|
+
}
|
|
69
121
|
return originalRpc(fn, contextArgs, options);
|
|
70
122
|
};
|
|
71
123
|
}
|
|
@@ -79,6 +131,10 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
79
131
|
* This avoids interfering with the main client's operations.
|
|
80
132
|
*/
|
|
81
133
|
setupEdgeFunctionHandling() {
|
|
134
|
+
if (this.usesExistingClient) {
|
|
135
|
+
this.edgeFunctionClient = null;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
82
138
|
this.edgeFunctionClient = createClient(this.supabaseUrl, this.supabaseKey);
|
|
83
139
|
}
|
|
84
140
|
/**
|
|
@@ -99,6 +155,7 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
99
155
|
const originalDelete = query.delete.bind(query);
|
|
100
156
|
query.select = (columns) => {
|
|
101
157
|
const result = originalSelect(columns);
|
|
158
|
+
result._tableName = tableName;
|
|
102
159
|
return this.addOrganisationFilter(result, tableName);
|
|
103
160
|
};
|
|
104
161
|
query.insert = (values) => {
|
|
@@ -119,9 +176,18 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
119
176
|
if (this.isSuperAdmin) {
|
|
120
177
|
return originalInsert(values);
|
|
121
178
|
}
|
|
179
|
+
if (!this.organisationId) {
|
|
180
|
+
throw new OrganisationContextRequiredError();
|
|
181
|
+
}
|
|
122
182
|
const contextValues2 = Array.isArray(values) ? values.map((v) => ({ ...v, organisation_id: this.organisationId })) : { ...values, organisation_id: this.organisationId };
|
|
123
183
|
return originalInsert(contextValues2);
|
|
124
184
|
}
|
|
185
|
+
if (this.isSuperAdmin && !this.organisationId) {
|
|
186
|
+
return originalInsert(values);
|
|
187
|
+
}
|
|
188
|
+
if (!this.organisationId) {
|
|
189
|
+
throw new OrganisationContextRequiredError();
|
|
190
|
+
}
|
|
125
191
|
const contextValues = Array.isArray(values) ? values.map((v) => ({ ...v, organisation_id: this.organisationId })) : { ...values, organisation_id: this.organisationId };
|
|
126
192
|
return originalInsert(contextValues);
|
|
127
193
|
};
|
|
@@ -146,6 +212,10 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
146
212
|
* - Super admins: No org filter (see all users) - RLS will allow access
|
|
147
213
|
* - Non-super-admins: Apply org filter as defense in depth - RLS will also filter
|
|
148
214
|
*
|
|
215
|
+
* For system-wide tables (like core_organisations):
|
|
216
|
+
* - Super admins: No org filter (see all records) - RLS will allow access
|
|
217
|
+
* - Non-super-admins: Apply org filter as defense in depth - RLS will also filter
|
|
218
|
+
*
|
|
149
219
|
* For other tables:
|
|
150
220
|
* - Always apply org filter unless super admin bypasses it
|
|
151
221
|
*/
|
|
@@ -189,11 +259,18 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
189
259
|
if (!this.organisationId) {
|
|
190
260
|
return query;
|
|
191
261
|
}
|
|
262
|
+
const systemWideTablesForSuperAdmins = [
|
|
263
|
+
"core_organisations"
|
|
264
|
+
// Super-admins need to see all organisations
|
|
265
|
+
];
|
|
266
|
+
if (systemWideTablesForSuperAdmins.includes(tableName) && this.isSuperAdmin) {
|
|
267
|
+
return query;
|
|
268
|
+
}
|
|
192
269
|
if (tableName === "rbac_user_profiles") {
|
|
193
270
|
if (this.isSuperAdmin) {
|
|
194
271
|
return query;
|
|
195
272
|
}
|
|
196
|
-
return query.
|
|
273
|
+
return query.or(`organisation_id.eq.${this.organisationId},organisation_id.is.null`);
|
|
197
274
|
}
|
|
198
275
|
if (this.isSuperAdmin) {
|
|
199
276
|
return query.eq("organisation_id", this.organisationId);
|
|
@@ -202,12 +279,57 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
202
279
|
}
|
|
203
280
|
/**
|
|
204
281
|
* Validate that required context is present
|
|
282
|
+
* Super-admins can operate without organisation context
|
|
205
283
|
*/
|
|
206
284
|
validateContext() {
|
|
285
|
+
if (this.isSuperAdmin) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
207
288
|
if (!this.organisationId) {
|
|
208
289
|
throw new OrganisationContextRequiredError();
|
|
209
290
|
}
|
|
210
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Determine whether a table requires organisation context.
|
|
294
|
+
* Tables without an organisation_id column (or global configuration tables) are safe without org context.
|
|
295
|
+
*/
|
|
296
|
+
tableRequiresOrganisationContext(tableName) {
|
|
297
|
+
const tablesWithoutOrganisationId = /* @__PURE__ */ new Set([
|
|
298
|
+
"core_organisations",
|
|
299
|
+
"rbac_apps",
|
|
300
|
+
"rbac_app_pages",
|
|
301
|
+
"rbac_global_roles",
|
|
302
|
+
"core_person",
|
|
303
|
+
"core_member",
|
|
304
|
+
"core_contact",
|
|
305
|
+
"core_consent",
|
|
306
|
+
"core_identification",
|
|
307
|
+
"core_qualification",
|
|
308
|
+
"medi_profile",
|
|
309
|
+
"medi_condition",
|
|
310
|
+
"medi_diet",
|
|
311
|
+
"medi_action_plan",
|
|
312
|
+
"medi_profile_versions"
|
|
313
|
+
]);
|
|
314
|
+
return !tablesWithoutOrganisationId.has(tableName);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Validate context for a specific table operation.
|
|
318
|
+
*/
|
|
319
|
+
validateContextForTable(tableName) {
|
|
320
|
+
if (this.isSuperAdmin) return;
|
|
321
|
+
if (!this.organisationId && this.tableRequiresOrganisationContext(tableName)) {
|
|
322
|
+
throw new OrganisationContextRequiredError();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Validate context for a specific RPC call.
|
|
327
|
+
*/
|
|
328
|
+
validateContextForRpc(fn) {
|
|
329
|
+
if (this.isSuperAdmin) return;
|
|
330
|
+
if (_SecureSupabaseClient.GLOBAL_RPC_ALLOWLIST.has(fn)) return;
|
|
331
|
+
this.validateContext();
|
|
332
|
+
}
|
|
211
333
|
/**
|
|
212
334
|
* Get the current organisation ID
|
|
213
335
|
*/
|
|
@@ -233,7 +355,7 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
233
355
|
return new _SecureSupabaseClient(
|
|
234
356
|
this.supabaseUrl,
|
|
235
357
|
this.supabaseKey,
|
|
236
|
-
updates.organisationId
|
|
358
|
+
updates.organisationId !== void 0 ? updates.organisationId : this.organisationId,
|
|
237
359
|
updates.eventId !== void 0 ? updates.eventId : this.eventId,
|
|
238
360
|
updates.appId !== void 0 ? updates.appId : this.appId,
|
|
239
361
|
updates.isSuperAdmin !== void 0 ? updates.isSuperAdmin : this.isSuperAdmin
|
|
@@ -244,7 +366,7 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
244
366
|
* @internal
|
|
245
367
|
*/
|
|
246
368
|
getClient() {
|
|
247
|
-
|
|
369
|
+
const proxiedClient = new Proxy(this.supabase, {
|
|
248
370
|
get: (target, prop) => {
|
|
249
371
|
if (prop === "functions" && this.edgeFunctionClient) {
|
|
250
372
|
return this.edgeFunctionClient.functions;
|
|
@@ -252,17 +374,248 @@ var SecureSupabaseClient = class _SecureSupabaseClient {
|
|
|
252
374
|
return target[prop];
|
|
253
375
|
}
|
|
254
376
|
});
|
|
377
|
+
markClientAsSecure(proxiedClient);
|
|
378
|
+
return proxiedClient;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get the set of parameter names that an RPC function accepts.
|
|
382
|
+
* Uses a static whitelist of RPCs that we know accept context parameters.
|
|
383
|
+
*
|
|
384
|
+
* This is an opt-in approach: by default, we don't inject context unless
|
|
385
|
+
* the function is explicitly whitelisted. This prevents PGRST202 errors from
|
|
386
|
+
* injecting unexpected parameters.
|
|
387
|
+
*
|
|
388
|
+
* @param fn - The RPC function name
|
|
389
|
+
* @returns Set of parameter names the function accepts
|
|
390
|
+
*/
|
|
391
|
+
getRpcAcceptedParams(fn) {
|
|
392
|
+
if (_SecureSupabaseClient.rpcSignatureCache.has(fn)) {
|
|
393
|
+
return _SecureSupabaseClient.rpcSignatureCache.get(fn);
|
|
394
|
+
}
|
|
395
|
+
const rpcContextWhitelist = {
|
|
396
|
+
// RPCs that accept all three context parameters
|
|
397
|
+
"rbac_roles_list": /* @__PURE__ */ new Set(["p_organisation_id", "p_event_id", "p_app_id"]),
|
|
398
|
+
// RPCs that accept only p_organisation_id (not p_app_id or p_event_id)
|
|
399
|
+
"data_file_reference_by_category_list": /* @__PURE__ */ new Set(["p_organisation_id"])
|
|
400
|
+
// Add more RPCs here as we discover them
|
|
401
|
+
// Format: 'function_name': new Set(['p_organisation_id', 'p_event_id', 'p_app_id']),
|
|
402
|
+
};
|
|
403
|
+
const acceptedParams = rpcContextWhitelist[fn] || /* @__PURE__ */ new Set();
|
|
404
|
+
_SecureSupabaseClient.rpcSignatureCache.set(fn, acceptedParams);
|
|
405
|
+
return acceptedParams;
|
|
255
406
|
}
|
|
256
407
|
};
|
|
408
|
+
// Cache for RPC function signatures to avoid repeated database queries
|
|
409
|
+
// Maps function name -> Set of parameter names it accepts
|
|
410
|
+
_SecureSupabaseClient.rpcSignatureCache = /* @__PURE__ */ new Map();
|
|
411
|
+
/**
|
|
412
|
+
* RPC functions that are safe to call without organisation context.
|
|
413
|
+
*
|
|
414
|
+
* These functions must:
|
|
415
|
+
* - rely on JWT context (auth.uid()) for authentication
|
|
416
|
+
* - not read or write organisation-scoped data
|
|
417
|
+
*
|
|
418
|
+
* This allowlist enables compliant consuming apps to use `secureSupabase.rpc(...)`
|
|
419
|
+
* even before an organisation is selected (common during initial page load/refresh).
|
|
420
|
+
*/
|
|
421
|
+
_SecureSupabaseClient.GLOBAL_RPC_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
422
|
+
"data_rbac_apps_list"
|
|
423
|
+
]);
|
|
424
|
+
var SecureSupabaseClient = _SecureSupabaseClient;
|
|
257
425
|
function createSecureClient(supabaseUrl, supabaseKey, organisationId, eventId, appId, isSuperAdmin = false) {
|
|
258
426
|
return new SecureSupabaseClient(supabaseUrl, supabaseKey, organisationId, eventId, appId, isSuperAdmin);
|
|
259
427
|
}
|
|
260
|
-
function fromSupabaseClient(client, organisationId, eventId, appId) {
|
|
261
|
-
|
|
428
|
+
function fromSupabaseClient(client, organisationId, eventId, appId, isSuperAdmin = false) {
|
|
429
|
+
return new SecureSupabaseClient("", "", organisationId, eventId, appId, isSuperAdmin, client);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/rbac/hooks/useResolvedScope.ts
|
|
433
|
+
import { useEffect, useState, useRef, useMemo } from "react";
|
|
434
|
+
var log = createLogger("useResolvedScope");
|
|
435
|
+
var appIdCache = /* @__PURE__ */ new Map();
|
|
436
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
437
|
+
function useResolvedScope({
|
|
438
|
+
supabase,
|
|
439
|
+
selectedOrganisationId,
|
|
440
|
+
selectedEventId
|
|
441
|
+
}) {
|
|
442
|
+
const [resolvedScope, setResolvedScope] = useState(null);
|
|
443
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
444
|
+
const [error, setError] = useState(null);
|
|
445
|
+
const stableScopeRef = useRef({
|
|
446
|
+
organisationId: "",
|
|
447
|
+
appId: "",
|
|
448
|
+
eventId: void 0
|
|
449
|
+
});
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
if (resolvedScope) {
|
|
452
|
+
const newScope = {
|
|
453
|
+
organisationId: resolvedScope.organisationId || "",
|
|
454
|
+
appId: resolvedScope.appId || "",
|
|
455
|
+
eventId: resolvedScope.eventId
|
|
456
|
+
};
|
|
457
|
+
if (stableScopeRef.current.organisationId !== newScope.organisationId || stableScopeRef.current.eventId !== newScope.eventId || stableScopeRef.current.appId !== newScope.appId) {
|
|
458
|
+
stableScopeRef.current = {
|
|
459
|
+
organisationId: newScope.organisationId,
|
|
460
|
+
appId: newScope.appId,
|
|
461
|
+
eventId: newScope.eventId
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
stableScopeRef.current = { organisationId: "", appId: "", eventId: void 0 };
|
|
466
|
+
}
|
|
467
|
+
}, [resolvedScope]);
|
|
468
|
+
const appName = getCurrentAppName();
|
|
469
|
+
const stableScope = stableScopeRef.current;
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
let cancelled = false;
|
|
472
|
+
const resolveScope = async () => {
|
|
473
|
+
if (!supabase && !selectedOrganisationId && !selectedEventId) {
|
|
474
|
+
if (!cancelled) {
|
|
475
|
+
setResolvedScope(null);
|
|
476
|
+
setIsLoading(false);
|
|
477
|
+
setError(null);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
setIsLoading(true);
|
|
482
|
+
setError(null);
|
|
483
|
+
try {
|
|
484
|
+
const appName2 = getCurrentAppName();
|
|
485
|
+
let appId = void 0;
|
|
486
|
+
if (supabase && appName2) {
|
|
487
|
+
try {
|
|
488
|
+
const { data: session } = await supabase.auth.getSession();
|
|
489
|
+
if (!session?.session) {
|
|
490
|
+
log.debug(`Skipping app resolution for "${appName2}" - user not authenticated`);
|
|
491
|
+
} else {
|
|
492
|
+
const cached = appIdCache.get(appName2);
|
|
493
|
+
const now = Date.now();
|
|
494
|
+
if (cached && now - cached.timestamp < CACHE_TTL) {
|
|
495
|
+
appId = cached.appId;
|
|
496
|
+
} else {
|
|
497
|
+
const { data: app, error: error2 } = await supabase.from("rbac_apps").select("id, name, is_active").eq("name", appName2).eq("is_active", true).single();
|
|
498
|
+
if (error2) {
|
|
499
|
+
if (error2.code === "406" || error2.code === "PGRST116" || error2.message?.includes("406")) {
|
|
500
|
+
log.debug(`App resolution blocked by RLS for "${appName2}" - user may not be authenticated`);
|
|
501
|
+
appId = void 0;
|
|
502
|
+
} else {
|
|
503
|
+
const { data: inactiveApp } = await supabase.from("rbac_apps").select("id, name, is_active").eq("name", appName2).single();
|
|
504
|
+
if (inactiveApp) {
|
|
505
|
+
log.error(`App "${appName2}" exists but is inactive (is_active: ${inactiveApp.is_active})`);
|
|
506
|
+
appId = void 0;
|
|
507
|
+
} else {
|
|
508
|
+
log.error(`App "${appName2}" not found in rbac_apps table`, { error: error2 });
|
|
509
|
+
appId = void 0;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} else if (app) {
|
|
513
|
+
appId = app.id;
|
|
514
|
+
appIdCache.set(appName2, { appId, timestamp: now });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
} catch (error2) {
|
|
519
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
520
|
+
if (!errorMessage.includes("406") && !errorMessage.includes("PGRST116")) {
|
|
521
|
+
log.error("Unexpected error resolving app ID:", error2);
|
|
522
|
+
} else {
|
|
523
|
+
log.debug("App resolution skipped - authentication required");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const initialScope = {
|
|
528
|
+
organisationId: selectedOrganisationId || void 0,
|
|
529
|
+
eventId: selectedEventId || void 0,
|
|
530
|
+
appId
|
|
531
|
+
};
|
|
532
|
+
if (appName2 === "PORTAL" || appName2 === "ADMIN") {
|
|
533
|
+
if (!cancelled) {
|
|
534
|
+
const optionalContextScope = {
|
|
535
|
+
organisationId: void 0,
|
|
536
|
+
eventId: void 0,
|
|
537
|
+
appId: appId || void 0
|
|
538
|
+
};
|
|
539
|
+
setResolvedScope(optionalContextScope);
|
|
540
|
+
setError(null);
|
|
541
|
+
setIsLoading(false);
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const { ContextValidator: ContextValidator2 } = await import("./contextValidator-3JNZKUTX.js");
|
|
546
|
+
const validation = await ContextValidator2.resolveScopeForPage(
|
|
547
|
+
initialScope,
|
|
548
|
+
"organisation",
|
|
549
|
+
// Default to organisation scope when no page context
|
|
550
|
+
appName2 || void 0,
|
|
551
|
+
supabase
|
|
552
|
+
);
|
|
553
|
+
if (!validation.isValid) {
|
|
554
|
+
if (selectedEventId) {
|
|
555
|
+
if (!cancelled) {
|
|
556
|
+
const eventScope = {
|
|
557
|
+
organisationId: void 0,
|
|
558
|
+
// Will be derived from event during permission check
|
|
559
|
+
eventId: selectedEventId,
|
|
560
|
+
appId: appId || void 0
|
|
561
|
+
};
|
|
562
|
+
setResolvedScope(eventScope);
|
|
563
|
+
setError(null);
|
|
564
|
+
setIsLoading(false);
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (!cancelled) {
|
|
569
|
+
setResolvedScope(null);
|
|
570
|
+
setError(validation.error || new Error("Context validation failed"));
|
|
571
|
+
setIsLoading(false);
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (!cancelled) {
|
|
576
|
+
setResolvedScope(validation.resolvedScope);
|
|
577
|
+
setError(null);
|
|
578
|
+
setIsLoading(false);
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
if (!cancelled) {
|
|
582
|
+
setError(err);
|
|
583
|
+
setIsLoading(false);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
resolveScope();
|
|
588
|
+
return () => {
|
|
589
|
+
cancelled = true;
|
|
590
|
+
};
|
|
591
|
+
}, [selectedOrganisationId, selectedEventId, supabase]);
|
|
592
|
+
const allowsOptionalContexts = appName === "PORTAL" || appName === "ADMIN";
|
|
593
|
+
const hasValidScope = allowsOptionalContexts ? true : stableScope.appId || stableScope.organisationId;
|
|
594
|
+
const finalScope = useMemo(() => {
|
|
595
|
+
if (!hasValidScope) {
|
|
596
|
+
return allowsOptionalContexts ? {} : null;
|
|
597
|
+
}
|
|
598
|
+
const scope = {};
|
|
599
|
+
if (stableScope.organisationId) {
|
|
600
|
+
scope.organisationId = stableScope.organisationId;
|
|
601
|
+
}
|
|
602
|
+
if (stableScope.eventId) {
|
|
603
|
+
scope.eventId = stableScope.eventId;
|
|
604
|
+
}
|
|
605
|
+
if (stableScope.appId) {
|
|
606
|
+
scope.appId = stableScope.appId;
|
|
607
|
+
}
|
|
608
|
+
return scope;
|
|
609
|
+
}, [hasValidScope, allowsOptionalContexts, stableScope.organisationId, stableScope.eventId, stableScope.appId]);
|
|
610
|
+
return {
|
|
611
|
+
resolvedScope: finalScope,
|
|
612
|
+
isLoading,
|
|
613
|
+
error
|
|
614
|
+
};
|
|
262
615
|
}
|
|
263
616
|
|
|
264
617
|
// src/rbac/hooks/useRBAC.ts
|
|
265
|
-
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
618
|
+
import { useState as useState2, useEffect as useEffect2, useCallback, useMemo as useMemo2 } from "react";
|
|
266
619
|
function mapAccessLevelToEventRole(level) {
|
|
267
620
|
switch (level) {
|
|
268
621
|
case "viewer":
|
|
@@ -285,7 +638,6 @@ function useRBAC(pageId) {
|
|
|
285
638
|
session,
|
|
286
639
|
supabase,
|
|
287
640
|
appName,
|
|
288
|
-
appConfig,
|
|
289
641
|
appId: contextAppId,
|
|
290
642
|
selectedOrganisation,
|
|
291
643
|
isContextReady: orgContextReady,
|
|
@@ -293,13 +645,13 @@ function useRBAC(pageId) {
|
|
|
293
645
|
selectedEvent,
|
|
294
646
|
eventLoading
|
|
295
647
|
} = useUnifiedAuth();
|
|
296
|
-
const [globalRole, setGlobalRole] =
|
|
297
|
-
const [organisationRole, setOrganisationRole] =
|
|
298
|
-
const [eventAppRole, setEventAppRole] =
|
|
299
|
-
const [permissionMap, setPermissionMap] =
|
|
300
|
-
const [currentScope, setCurrentScope] =
|
|
301
|
-
const [isLoading, setIsLoading] =
|
|
302
|
-
const [error, setError] =
|
|
648
|
+
const [globalRole, setGlobalRole] = useState2(null);
|
|
649
|
+
const [organisationRole, setOrganisationRole] = useState2(null);
|
|
650
|
+
const [eventAppRole, setEventAppRole] = useState2(null);
|
|
651
|
+
const [permissionMap, setPermissionMap] = useState2({});
|
|
652
|
+
const [currentScope, setCurrentScope] = useState2(null);
|
|
653
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
654
|
+
const [error, setError] = useState2(null);
|
|
303
655
|
const resetState = useCallback(() => {
|
|
304
656
|
setGlobalRole(null);
|
|
305
657
|
setOrganisationRole(null);
|
|
@@ -314,20 +666,15 @@ function useRBAC(pageId) {
|
|
|
314
666
|
return;
|
|
315
667
|
}
|
|
316
668
|
const initialScope = {
|
|
317
|
-
organisationId:
|
|
669
|
+
organisationId: selectedEvent?.organisation_id || selectedOrganisation?.id || void 0,
|
|
318
670
|
eventId: selectedEvent?.event_id || void 0,
|
|
319
671
|
appId: void 0
|
|
320
672
|
};
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
!!selectedOrganisation
|
|
327
|
-
);
|
|
328
|
-
if (appName !== "PORTAL" && appName !== "ADMIN" && !contextReady) {
|
|
329
|
-
setIsLoading(true);
|
|
330
|
-
return;
|
|
673
|
+
if (appName !== "PORTAL" && appName !== "ADMIN") {
|
|
674
|
+
if (!selectedOrganisation && !selectedEvent) {
|
|
675
|
+
setIsLoading(true);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
331
678
|
}
|
|
332
679
|
setIsLoading(true);
|
|
333
680
|
setError(null);
|
|
@@ -338,11 +685,6 @@ function useRBAC(pageId) {
|
|
|
338
685
|
const resolved = await resolveAppContext({ userId: user.id, appName });
|
|
339
686
|
if (!resolved) {
|
|
340
687
|
if (appName === "PORTAL" || appName === "ADMIN") {
|
|
341
|
-
try {
|
|
342
|
-
const { getAppConfigByName } = await import("./api-N774RPUA.js");
|
|
343
|
-
await getAppConfigByName(appName);
|
|
344
|
-
} catch (err) {
|
|
345
|
-
}
|
|
346
688
|
} else {
|
|
347
689
|
throw new Error(`User does not have access to app "${appName}"`);
|
|
348
690
|
}
|
|
@@ -372,9 +714,20 @@ function useRBAC(pageId) {
|
|
|
372
714
|
...initialScope,
|
|
373
715
|
appId: appId || contextAppId
|
|
374
716
|
};
|
|
375
|
-
|
|
717
|
+
let pageScopeType = "organisation";
|
|
718
|
+
if (pageId && scope.appId) {
|
|
719
|
+
try {
|
|
720
|
+
pageScopeType = await getPageScopeType(pageId, scope.appId, appName);
|
|
721
|
+
} catch (error2) {
|
|
722
|
+
logger2.warn("[useRBAC] Failed to get page scope type, defaulting to organisation", {
|
|
723
|
+
pageId,
|
|
724
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const validation = await ContextValidator.resolveScopeForPage(
|
|
376
729
|
scope,
|
|
377
|
-
|
|
730
|
+
pageScopeType,
|
|
378
731
|
appName,
|
|
379
732
|
supabase || null
|
|
380
733
|
);
|
|
@@ -384,9 +737,9 @@ function useRBAC(pageId) {
|
|
|
384
737
|
const resolvedScope = validation.resolvedScope;
|
|
385
738
|
setCurrentScope(resolvedScope);
|
|
386
739
|
const [map, roleContext, accessLevel] = await Promise.all([
|
|
387
|
-
getPermissionMap({ userId: user.id, scope: resolvedScope }
|
|
388
|
-
getRoleContext({ userId: user.id, scope: resolvedScope }
|
|
389
|
-
getAccessLevel({ userId: user.id, scope: resolvedScope }
|
|
740
|
+
getPermissionMap({ userId: user.id, scope: resolvedScope }),
|
|
741
|
+
getRoleContext({ userId: user.id, scope: resolvedScope }),
|
|
742
|
+
getAccessLevel({ userId: user.id, scope: resolvedScope })
|
|
390
743
|
]);
|
|
391
744
|
setPermissionMap(map);
|
|
392
745
|
setGlobalRole(roleContext.globalRole);
|
|
@@ -408,7 +761,7 @@ function useRBAC(pageId) {
|
|
|
408
761
|
} finally {
|
|
409
762
|
setIsLoading(false);
|
|
410
763
|
}
|
|
411
|
-
}, [appName, logger2, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user, eventLoading,
|
|
764
|
+
}, [appName, logger2, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user, eventLoading, orgContextReady, orgLoading]);
|
|
412
765
|
const hasGlobalPermission = useCallback(
|
|
413
766
|
(permission) => {
|
|
414
767
|
if (globalRole === "super_admin" || permissionMap["*"]) {
|
|
@@ -424,14 +777,14 @@ function useRBAC(pageId) {
|
|
|
424
777
|
},
|
|
425
778
|
[globalRole, organisationRole, permissionMap]
|
|
426
779
|
);
|
|
427
|
-
const isSuperAdmin =
|
|
428
|
-
const isOrgAdmin =
|
|
429
|
-
const isEventAdmin =
|
|
430
|
-
const canManageOrganisation =
|
|
431
|
-
const canManageEvent =
|
|
432
|
-
|
|
780
|
+
const isSuperAdmin = useMemo2(() => globalRole === "super_admin" || permissionMap["*"] === true, [globalRole, permissionMap]);
|
|
781
|
+
const isOrgAdmin = useMemo2(() => organisationRole === "org_admin" || isSuperAdmin, [organisationRole, isSuperAdmin]);
|
|
782
|
+
const isEventAdmin = useMemo2(() => eventAppRole === "event_admin" || isSuperAdmin, [eventAppRole, isSuperAdmin]);
|
|
783
|
+
const canManageOrganisation = useMemo2(() => isSuperAdmin || organisationRole === "org_admin", [isSuperAdmin, organisationRole]);
|
|
784
|
+
const canManageEvent = useMemo2(() => isSuperAdmin || eventAppRole === "event_admin", [isSuperAdmin, eventAppRole]);
|
|
785
|
+
useEffect2(() => {
|
|
433
786
|
loadRBACContext();
|
|
434
|
-
}, [loadRBACContext, appName,
|
|
787
|
+
}, [loadRBACContext, appName, eventLoading, selectedEvent?.event_id, user, session, selectedOrganisation?.id, orgContextReady, orgLoading]);
|
|
435
788
|
return {
|
|
436
789
|
user,
|
|
437
790
|
globalRole,
|
|
@@ -448,197 +801,191 @@ function useRBAC(pageId) {
|
|
|
448
801
|
};
|
|
449
802
|
}
|
|
450
803
|
|
|
451
|
-
// src/rbac/hooks/
|
|
452
|
-
import {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
804
|
+
// src/rbac/hooks/permissions/useAccessLevel.ts
|
|
805
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo3, useState as useState3 } from "react";
|
|
806
|
+
function useAccessLevel(userId, scope) {
|
|
807
|
+
const [accessLevel, setAccessLevel] = useState3("viewer");
|
|
808
|
+
const [isLoading, setIsLoading] = useState3(true);
|
|
809
|
+
const [error, setError] = useState3(null);
|
|
810
|
+
let appName;
|
|
811
|
+
try {
|
|
812
|
+
const { appName: contextAppName } = useAppConfig();
|
|
813
|
+
appName = contextAppName;
|
|
814
|
+
} catch {
|
|
461
815
|
}
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// src/rbac/hooks/usePermissions.ts
|
|
466
|
-
function usePermissions(userId, organisationId, eventId, appId) {
|
|
467
|
-
const [permissions, setPermissions] = useState2({});
|
|
468
|
-
const [isLoading, setIsLoading] = useState2(true);
|
|
469
|
-
const [error, setError] = useState2(null);
|
|
470
|
-
const [fetchTrigger, setFetchTrigger] = useState2(0);
|
|
471
|
-
const isFetchingRef = useRef(false);
|
|
472
|
-
const logger2 = getRBACLogger();
|
|
473
|
-
const prevValuesRef = useRef({ userId, organisationId, eventId, appId });
|
|
474
|
-
const orgId = organisationId || "";
|
|
475
|
-
useEffect2(() => {
|
|
816
|
+
const fetchAccessLevel = useCallback2(async () => {
|
|
476
817
|
if (!userId) {
|
|
818
|
+
setAccessLevel("viewer");
|
|
819
|
+
setIsLoading(false);
|
|
477
820
|
return;
|
|
478
821
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
setError(new Error("Organisation context is required for permission checks"));
|
|
482
|
-
setIsLoading(false);
|
|
483
|
-
}, 3e3);
|
|
484
|
-
return () => clearTimeout(timeoutId);
|
|
485
|
-
}
|
|
486
|
-
if (error?.message === "Organisation context is required for permission checks") {
|
|
822
|
+
try {
|
|
823
|
+
setIsLoading(true);
|
|
487
824
|
setError(null);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (paramsChanged) {
|
|
493
|
-
if (prevValuesRef.current.appId !== appId) {
|
|
494
|
-
}
|
|
495
|
-
prevValuesRef.current = { userId, organisationId, eventId, appId };
|
|
496
|
-
setFetchTrigger((prev) => prev + 1);
|
|
497
|
-
}
|
|
498
|
-
}, [userId, organisationId, eventId, appId, logger2]);
|
|
499
|
-
useEffect2(() => {
|
|
500
|
-
const fetchPermissions = async () => {
|
|
501
|
-
if (isFetchingRef.current) {
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
if (!userId) {
|
|
505
|
-
setPermissions({});
|
|
825
|
+
const { isSuperAdmin: checkSuperAdmin } = await import("./api-IAGWF3ZG.js");
|
|
826
|
+
const isSuperAdminUser = await checkSuperAdmin(userId);
|
|
827
|
+
if (isSuperAdminUser) {
|
|
828
|
+
setAccessLevel("super");
|
|
506
829
|
setIsLoading(false);
|
|
507
830
|
return;
|
|
508
831
|
}
|
|
509
|
-
if (!
|
|
510
|
-
|
|
832
|
+
if (appName !== "PORTAL" && appName !== "ADMIN" && !scope.organisationId && !scope.eventId) {
|
|
833
|
+
const orgError = new OrganisationContextRequiredError();
|
|
834
|
+
setError(orgError);
|
|
835
|
+
setAccessLevel("viewer");
|
|
511
836
|
setIsLoading(false);
|
|
512
837
|
return;
|
|
513
838
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
setError(null);
|
|
523
|
-
const scope = {
|
|
524
|
-
organisationId: orgId,
|
|
525
|
-
eventId,
|
|
526
|
-
appId
|
|
527
|
-
};
|
|
528
|
-
const permissionMap = await getPermissionMap({ userId, scope });
|
|
529
|
-
const permissionCount = Object.keys(permissionMap).length;
|
|
530
|
-
if (permissionCount === 0 && Object.keys(permissions).length > 0) {
|
|
531
|
-
logger2.warn("[usePermissions] Permissions fetched but returned empty map", {
|
|
532
|
-
scope: { organisationId: orgId, eventId, appId }
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
setPermissions(permissionMap);
|
|
536
|
-
} catch (err) {
|
|
537
|
-
logger2.error("[usePermissions] Failed to fetch permissions:", err);
|
|
538
|
-
setError(err instanceof Error ? err : new Error("Failed to fetch permissions"));
|
|
539
|
-
} finally {
|
|
540
|
-
setIsLoading(false);
|
|
541
|
-
isFetchingRef.current = false;
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
fetchPermissions();
|
|
545
|
-
}, [fetchTrigger, userId, organisationId, eventId, appId]);
|
|
546
|
-
const hasPermission = useCallback2((permission) => {
|
|
547
|
-
if (permissions["*"]) {
|
|
548
|
-
return true;
|
|
549
|
-
}
|
|
550
|
-
return permissions[permission] === true;
|
|
551
|
-
}, [permissions]);
|
|
552
|
-
const hasAnyPermission = useCallback2((permissionList) => {
|
|
553
|
-
if (permissions["*"]) {
|
|
554
|
-
return true;
|
|
555
|
-
}
|
|
556
|
-
return permissionList.some((p) => permissions[p] === true);
|
|
557
|
-
}, [permissions]);
|
|
558
|
-
const hasAllPermissions = useCallback2((permissionList) => {
|
|
559
|
-
if (permissions["*"]) {
|
|
560
|
-
return true;
|
|
561
|
-
}
|
|
562
|
-
return permissionList.every((p) => permissions[p] === true);
|
|
563
|
-
}, [permissions]);
|
|
564
|
-
const refetch = useCallback2(async () => {
|
|
565
|
-
if (isFetchingRef.current) {
|
|
566
|
-
return;
|
|
839
|
+
const level = await getAccessLevel({ userId, scope }, appName);
|
|
840
|
+
setAccessLevel(level);
|
|
841
|
+
} catch (err) {
|
|
842
|
+
const error2 = err instanceof Error ? err : new Error("Failed to fetch access level");
|
|
843
|
+
setError(error2);
|
|
844
|
+
setAccessLevel("viewer");
|
|
845
|
+
} finally {
|
|
846
|
+
setIsLoading(false);
|
|
567
847
|
}
|
|
848
|
+
}, [userId, scope.organisationId, scope.eventId, scope.appId, appName]);
|
|
849
|
+
useEffect3(() => {
|
|
850
|
+
fetchAccessLevel();
|
|
851
|
+
}, [fetchAccessLevel]);
|
|
852
|
+
return useMemo3(() => ({
|
|
853
|
+
accessLevel,
|
|
854
|
+
isLoading,
|
|
855
|
+
error,
|
|
856
|
+
refetch: fetchAccessLevel
|
|
857
|
+
}), [accessLevel, isLoading, error, fetchAccessLevel]);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/rbac/hooks/permissions/useCachedPermissions.ts
|
|
861
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
|
|
862
|
+
function useCachedPermissions(userId, scope) {
|
|
863
|
+
const [permissions, setPermissions] = useState4({});
|
|
864
|
+
const [isLoading, setIsLoading] = useState4(true);
|
|
865
|
+
const [error, setError] = useState4(null);
|
|
866
|
+
const fetchCachedPermissions = useCallback3(async () => {
|
|
568
867
|
if (!userId) {
|
|
569
868
|
setPermissions({});
|
|
570
869
|
setIsLoading(false);
|
|
571
870
|
return;
|
|
572
871
|
}
|
|
573
|
-
if (!orgId || orgId === null || typeof orgId === "string" && orgId.trim() === "") {
|
|
574
|
-
setIsLoading(true);
|
|
575
|
-
setError(null);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
872
|
try {
|
|
579
|
-
isFetchingRef.current = true;
|
|
580
873
|
setIsLoading(true);
|
|
581
874
|
setError(null);
|
|
582
|
-
const scope = {
|
|
583
|
-
organisationId: orgId,
|
|
584
|
-
eventId,
|
|
585
|
-
appId
|
|
586
|
-
};
|
|
587
875
|
const permissionMap = await getPermissionMap({ userId, scope });
|
|
588
876
|
setPermissions(permissionMap);
|
|
589
877
|
} catch (err) {
|
|
590
|
-
|
|
591
|
-
logger3.error("Failed to refetch permissions:", err);
|
|
592
|
-
setError(err instanceof Error ? err : new Error("Failed to fetch permissions"));
|
|
878
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch cached permissions"));
|
|
593
879
|
} finally {
|
|
594
880
|
setIsLoading(false);
|
|
595
|
-
isFetchingRef.current = false;
|
|
596
881
|
}
|
|
597
|
-
}, [userId, organisationId, eventId, appId]);
|
|
598
|
-
|
|
882
|
+
}, [userId, scope.organisationId, scope.eventId, scope.appId]);
|
|
883
|
+
const invalidateCache = useCallback3(() => {
|
|
884
|
+
fetchCachedPermissions();
|
|
885
|
+
}, [fetchCachedPermissions]);
|
|
886
|
+
useEffect4(() => {
|
|
887
|
+
fetchCachedPermissions();
|
|
888
|
+
}, [fetchCachedPermissions]);
|
|
889
|
+
return useMemo4(() => ({
|
|
599
890
|
permissions,
|
|
600
891
|
isLoading,
|
|
601
892
|
error,
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
refetch
|
|
606
|
-
}), [permissions, isLoading, error, hasPermission, hasAnyPermission, hasAllPermissions, refetch]);
|
|
893
|
+
invalidateCache,
|
|
894
|
+
refetch: fetchCachedPermissions
|
|
895
|
+
}), [permissions, isLoading, error, invalidateCache, fetchCachedPermissions]);
|
|
607
896
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
897
|
+
|
|
898
|
+
// src/rbac/hooks/permissions/useCan.ts
|
|
899
|
+
import { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo5, useRef as useRef2, useState as useState5 } from "react";
|
|
900
|
+
|
|
901
|
+
// src/rbac/utils/deep-equal.ts
|
|
902
|
+
function scopeEqual(a, b) {
|
|
903
|
+
if (a === b) {
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
if (a == null || b == null) {
|
|
907
|
+
return a === b;
|
|
908
|
+
}
|
|
909
|
+
return a.organisationId === b.organisationId && a.eventId === b.eventId && a.appId === b.appId;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/rbac/hooks/permissions/useCan.ts
|
|
913
|
+
function useCan(userId, scope, permission, pageId, useCache = true, precomputedSuperAdmin = null, appName) {
|
|
914
|
+
const [isSuperAdmin, setIsSuperAdmin] = useState5(precomputedSuperAdmin ?? null);
|
|
915
|
+
const initialCan = precomputedSuperAdmin === true ? true : false;
|
|
916
|
+
const initialIsLoading = precomputedSuperAdmin === true ? false : true;
|
|
917
|
+
const [can, setCan] = useState5(initialCan);
|
|
918
|
+
const [isLoading, setIsLoading] = useState5(initialIsLoading);
|
|
919
|
+
const [error, setError] = useState5(null);
|
|
613
920
|
const isValidScope = scope && typeof scope === "object";
|
|
614
921
|
const organisationId = isValidScope ? scope.organisationId : void 0;
|
|
615
922
|
const eventId = isValidScope ? scope.eventId : void 0;
|
|
616
923
|
const appId = isValidScope ? scope.appId : void 0;
|
|
617
|
-
|
|
618
|
-
if (
|
|
924
|
+
useEffect5(() => {
|
|
925
|
+
if (precomputedSuperAdmin === true && isSuperAdmin !== true) {
|
|
926
|
+
setIsSuperAdmin(true);
|
|
927
|
+
setCan(true);
|
|
928
|
+
setIsLoading(false);
|
|
929
|
+
setError(null);
|
|
930
|
+
} else if (precomputedSuperAdmin === false && isSuperAdmin !== false) {
|
|
619
931
|
setIsSuperAdmin(false);
|
|
620
|
-
return;
|
|
621
932
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
setIsSuperAdmin(isSuper);
|
|
629
|
-
}
|
|
630
|
-
} catch (err) {
|
|
631
|
-
if (!cancelled) {
|
|
632
|
-
setIsSuperAdmin(false);
|
|
633
|
-
}
|
|
933
|
+
}, [precomputedSuperAdmin, isSuperAdmin]);
|
|
934
|
+
useEffect5(() => {
|
|
935
|
+
if (precomputedSuperAdmin === null) {
|
|
936
|
+
if (!userId) {
|
|
937
|
+
setIsSuperAdmin(false);
|
|
938
|
+
return;
|
|
634
939
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
940
|
+
let cancelled = false;
|
|
941
|
+
const checkSuperAdmin = async () => {
|
|
942
|
+
const startTime = Date.now();
|
|
943
|
+
try {
|
|
944
|
+
const { isSuperAdmin: checkSuperAdmin2 } = await import("./api-IAGWF3ZG.js");
|
|
945
|
+
const timeoutWarning = setTimeout(() => {
|
|
946
|
+
if (!cancelled) {
|
|
947
|
+
console.warn("[useCan] Super admin check taking longer than 5 seconds", {
|
|
948
|
+
userId,
|
|
949
|
+
elapsedMs: Date.now() - startTime
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}, 5e3);
|
|
953
|
+
const isSuper = await checkSuperAdmin2(userId);
|
|
954
|
+
clearTimeout(timeoutWarning);
|
|
955
|
+
if (!cancelled) {
|
|
956
|
+
const elapsed = Date.now() - startTime;
|
|
957
|
+
if (elapsed > 1e3) {
|
|
958
|
+
console.warn("[useCan] Super admin check took longer than expected", {
|
|
959
|
+
userId,
|
|
960
|
+
elapsedMs: elapsed
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
setIsSuperAdmin(isSuper);
|
|
964
|
+
if (isSuper) {
|
|
965
|
+
setCan(true);
|
|
966
|
+
setIsLoading(false);
|
|
967
|
+
setError(null);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
} catch (err) {
|
|
971
|
+
if (!cancelled) {
|
|
972
|
+
const elapsed = Date.now() - startTime;
|
|
973
|
+
console.error("[useCan] Error checking super admin", {
|
|
974
|
+
userId,
|
|
975
|
+
error: err,
|
|
976
|
+
elapsedMs: elapsed
|
|
977
|
+
});
|
|
978
|
+
setIsSuperAdmin(false);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
checkSuperAdmin();
|
|
983
|
+
return () => {
|
|
984
|
+
cancelled = true;
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
}, [userId, precomputedSuperAdmin]);
|
|
988
|
+
useEffect5(() => {
|
|
642
989
|
const isPagePermission = permission.includes(":page.") || !!pageId;
|
|
643
990
|
const requiresOrgId = !isPagePermission;
|
|
644
991
|
if (isSuperAdmin === true) {
|
|
@@ -656,12 +1003,13 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
656
1003
|
setError(null);
|
|
657
1004
|
}
|
|
658
1005
|
}, [isValidScope, organisationId, error, permission, pageId, isSuperAdmin]);
|
|
659
|
-
const lastUserIdRef =
|
|
660
|
-
const lastScopeRef =
|
|
661
|
-
const lastPermissionRef =
|
|
662
|
-
const lastPageIdRef =
|
|
663
|
-
const lastUseCacheRef =
|
|
664
|
-
const
|
|
1006
|
+
const lastUserIdRef = useRef2(null);
|
|
1007
|
+
const lastScopeRef = useRef2(null);
|
|
1008
|
+
const lastPermissionRef = useRef2(null);
|
|
1009
|
+
const lastPageIdRef = useRef2(null);
|
|
1010
|
+
const lastUseCacheRef = useRef2(null);
|
|
1011
|
+
const lastIsSuperAdminRef = useRef2(null);
|
|
1012
|
+
const stableScope = useMemo5(() => {
|
|
665
1013
|
if (!isValidScope) {
|
|
666
1014
|
return null;
|
|
667
1015
|
}
|
|
@@ -671,10 +1019,12 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
671
1019
|
appId
|
|
672
1020
|
};
|
|
673
1021
|
}, [isValidScope, organisationId, eventId, appId]);
|
|
674
|
-
const prevScopeRef =
|
|
675
|
-
|
|
1022
|
+
const prevScopeRef = useRef2(null);
|
|
1023
|
+
useEffect5(() => {
|
|
676
1024
|
const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
|
|
677
|
-
|
|
1025
|
+
const isSuperAdminChanged = lastIsSuperAdminRef.current !== isSuperAdmin;
|
|
1026
|
+
if (lastUserIdRef.current !== userId || scopeChanged || lastPermissionRef.current !== permission || lastPageIdRef.current !== pageId || lastUseCacheRef.current !== useCache || isSuperAdminChanged) {
|
|
1027
|
+
lastIsSuperAdminRef.current = isSuperAdmin;
|
|
678
1028
|
lastUserIdRef.current = userId;
|
|
679
1029
|
prevScopeRef.current = stableScope;
|
|
680
1030
|
lastPermissionRef.current = permission;
|
|
@@ -683,7 +1033,19 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
683
1033
|
const checkPermission = async () => {
|
|
684
1034
|
if (!userId) {
|
|
685
1035
|
setCan(false);
|
|
686
|
-
setIsLoading(false);
|
|
1036
|
+
setIsLoading(false);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (isSuperAdmin === true) {
|
|
1040
|
+
setCan(true);
|
|
1041
|
+
setIsLoading(false);
|
|
1042
|
+
setError(null);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (isSuperAdmin === null) {
|
|
1046
|
+
setIsLoading(true);
|
|
1047
|
+
setCan(false);
|
|
1048
|
+
setError(null);
|
|
687
1049
|
return;
|
|
688
1050
|
}
|
|
689
1051
|
if (!isValidScope) {
|
|
@@ -697,13 +1059,10 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
697
1059
|
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);
|
|
698
1060
|
const needsAppIdForPageName = isPagePermission && isPageName;
|
|
699
1061
|
if (requiresOrgId && (!organisationId || organisationId === null || typeof organisationId === "string" && organisationId.trim() === "")) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
setError(null);
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
1062
|
+
setIsLoading(true);
|
|
1063
|
+
setCan(false);
|
|
1064
|
+
setError(null);
|
|
1065
|
+
return;
|
|
707
1066
|
}
|
|
708
1067
|
if (needsAppIdForPageName && (!appId || appId === null || typeof appId === "string" && appId.trim() === "")) {
|
|
709
1068
|
setIsLoading(true);
|
|
@@ -719,11 +1078,12 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
719
1078
|
...eventId ? { eventId } : {},
|
|
720
1079
|
...appId ? { appId } : {}
|
|
721
1080
|
};
|
|
722
|
-
const result = useCache ? await isPermittedCached({ userId, scope: validScope, permission, pageId },
|
|
1081
|
+
const result = useCache ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName) : await isPermitted({ userId, scope: validScope, permission, pageId }, appName, isSuperAdmin === false ? false : null);
|
|
723
1082
|
setCan(result);
|
|
724
1083
|
} catch (err) {
|
|
725
1084
|
const logger2 = getRBACLogger();
|
|
726
1085
|
logger2.error("Permission check error:", { permission, error: err });
|
|
1086
|
+
console.error("[useCan] Permission check error", { userId, permission, error: err });
|
|
727
1087
|
setError(err instanceof Error ? err : new Error("Failed to check permission"));
|
|
728
1088
|
setCan(false);
|
|
729
1089
|
} finally {
|
|
@@ -733,7 +1093,7 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
733
1093
|
checkPermission();
|
|
734
1094
|
}
|
|
735
1095
|
}, [userId, stableScope, permission, pageId, useCache, appName, isSuperAdmin]);
|
|
736
|
-
const refetch =
|
|
1096
|
+
const refetch = useCallback4(async () => {
|
|
737
1097
|
if (!userId) {
|
|
738
1098
|
setCan(false);
|
|
739
1099
|
setIsLoading(false);
|
|
@@ -761,7 +1121,7 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
761
1121
|
...eventId ? { eventId } : {},
|
|
762
1122
|
...appId ? { appId } : {}
|
|
763
1123
|
};
|
|
764
|
-
const result = useCache ? await isPermittedCached({ userId, scope: validScope, permission, pageId },
|
|
1124
|
+
const result = useCache ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName) : await isPermitted({ userId, scope: validScope, permission, pageId }, appName, null);
|
|
765
1125
|
setCan(result);
|
|
766
1126
|
} catch (err) {
|
|
767
1127
|
setError(err instanceof Error ? err : new Error("Failed to check permission"));
|
|
@@ -770,107 +1130,63 @@ function useCan(userId, scope, permission, pageId, useCache = true, appName) {
|
|
|
770
1130
|
setIsLoading(false);
|
|
771
1131
|
}
|
|
772
1132
|
}, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache, appName]);
|
|
773
|
-
return
|
|
1133
|
+
return useMemo5(() => ({
|
|
774
1134
|
can,
|
|
775
1135
|
isLoading,
|
|
776
1136
|
error,
|
|
777
1137
|
refetch
|
|
778
1138
|
}), [can, isLoading, error, refetch]);
|
|
779
1139
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
} catch {
|
|
789
|
-
}
|
|
790
|
-
const fetchAccessLevel = useCallback2(async () => {
|
|
791
|
-
if (!userId) {
|
|
792
|
-
setAccessLevel("viewer");
|
|
793
|
-
setIsLoading(false);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
try {
|
|
797
|
-
setIsLoading(true);
|
|
798
|
-
setError(null);
|
|
799
|
-
const { isSuperAdmin: checkSuperAdmin } = await import("./api-N774RPUA.js");
|
|
800
|
-
const isSuperAdminUser = await checkSuperAdmin(userId);
|
|
801
|
-
if (isSuperAdminUser) {
|
|
802
|
-
setAccessLevel("super");
|
|
803
|
-
setIsLoading(false);
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
if (appName !== "PORTAL" && appName !== "ADMIN" && !scope.organisationId && !scope.eventId) {
|
|
807
|
-
const orgError = new OrganisationContextRequiredError();
|
|
808
|
-
setError(orgError);
|
|
809
|
-
setAccessLevel("viewer");
|
|
810
|
-
setIsLoading(false);
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
const level = await getAccessLevel({ userId, scope }, null, appName);
|
|
814
|
-
setAccessLevel(level);
|
|
815
|
-
} catch (err) {
|
|
816
|
-
const error2 = err instanceof Error ? err : new Error("Failed to fetch access level");
|
|
817
|
-
setError(error2);
|
|
818
|
-
setAccessLevel("viewer");
|
|
819
|
-
} finally {
|
|
820
|
-
setIsLoading(false);
|
|
821
|
-
}
|
|
822
|
-
}, [userId, scope.organisationId, scope.eventId, scope.appId, appName]);
|
|
823
|
-
useEffect2(() => {
|
|
824
|
-
fetchAccessLevel();
|
|
825
|
-
}, [fetchAccessLevel]);
|
|
826
|
-
return useMemo2(() => ({
|
|
827
|
-
accessLevel,
|
|
828
|
-
isLoading,
|
|
829
|
-
error,
|
|
830
|
-
refetch: fetchAccessLevel
|
|
831
|
-
}), [accessLevel, isLoading, error, fetchAccessLevel]);
|
|
832
|
-
}
|
|
833
|
-
function useMultiplePermissions(userId, scope, permissions, useCache = true) {
|
|
834
|
-
const [results, setResults] = useState2({});
|
|
835
|
-
const [isLoading, setIsLoading] = useState2(true);
|
|
836
|
-
const [error, setError] = useState2(null);
|
|
837
|
-
const checkPermissions = useCallback2(async () => {
|
|
1140
|
+
|
|
1141
|
+
// src/rbac/hooks/permissions/useHasAllPermissions.ts
|
|
1142
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo6, useState as useState6 } from "react";
|
|
1143
|
+
function useHasAllPermissions(userId, scope, permissions, useCache = true) {
|
|
1144
|
+
const [hasAll, setHasAll] = useState6(false);
|
|
1145
|
+
const [isLoading, setIsLoading] = useState6(true);
|
|
1146
|
+
const [error, setError] = useState6(null);
|
|
1147
|
+
const checkAllPermissions = useCallback5(async () => {
|
|
838
1148
|
if (!userId || permissions.length === 0) {
|
|
839
|
-
|
|
1149
|
+
setHasAll(false);
|
|
840
1150
|
setIsLoading(false);
|
|
841
1151
|
return;
|
|
842
1152
|
}
|
|
843
1153
|
try {
|
|
844
1154
|
setIsLoading(true);
|
|
845
1155
|
setError(null);
|
|
846
|
-
|
|
1156
|
+
let hasAllPermissions = true;
|
|
847
1157
|
for (const permission of permissions) {
|
|
848
1158
|
const result = useCache ? await isPermittedCached({ userId, scope, permission }) : await isPermitted({ userId, scope, permission });
|
|
849
|
-
|
|
1159
|
+
if (!result) {
|
|
1160
|
+
hasAllPermissions = false;
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
850
1163
|
}
|
|
851
|
-
|
|
1164
|
+
setHasAll(hasAllPermissions);
|
|
852
1165
|
} catch (err) {
|
|
853
1166
|
setError(err instanceof Error ? err : new Error("Failed to check permissions"));
|
|
854
|
-
|
|
1167
|
+
setHasAll(false);
|
|
855
1168
|
} finally {
|
|
856
1169
|
setIsLoading(false);
|
|
857
1170
|
}
|
|
858
1171
|
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}, [
|
|
862
|
-
return
|
|
863
|
-
|
|
1172
|
+
useEffect6(() => {
|
|
1173
|
+
checkAllPermissions();
|
|
1174
|
+
}, [checkAllPermissions]);
|
|
1175
|
+
return useMemo6(() => ({
|
|
1176
|
+
hasAll,
|
|
864
1177
|
isLoading,
|
|
865
1178
|
error,
|
|
866
|
-
refetch:
|
|
867
|
-
}), [
|
|
1179
|
+
refetch: checkAllPermissions
|
|
1180
|
+
}), [hasAll, isLoading, error, checkAllPermissions]);
|
|
868
1181
|
}
|
|
1182
|
+
|
|
1183
|
+
// src/rbac/hooks/permissions/useHasAnyPermission.ts
|
|
1184
|
+
import { useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo7, useState as useState7 } from "react";
|
|
869
1185
|
function useHasAnyPermission(userId, scope, permissions, useCache = true) {
|
|
870
|
-
const [hasAny, setHasAny] =
|
|
871
|
-
const [isLoading, setIsLoading] =
|
|
872
|
-
const [error, setError] =
|
|
873
|
-
const checkAnyPermission =
|
|
1186
|
+
const [hasAny, setHasAny] = useState7(false);
|
|
1187
|
+
const [isLoading, setIsLoading] = useState7(true);
|
|
1188
|
+
const [error, setError] = useState7(null);
|
|
1189
|
+
const checkAnyPermission = useCallback6(async () => {
|
|
874
1190
|
if (!userId || permissions.length === 0) {
|
|
875
1191
|
setHasAny(false);
|
|
876
1192
|
setIsLoading(false);
|
|
@@ -895,93 +1211,203 @@ function useHasAnyPermission(userId, scope, permissions, useCache = true) {
|
|
|
895
1211
|
setIsLoading(false);
|
|
896
1212
|
}
|
|
897
1213
|
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
898
|
-
|
|
1214
|
+
useEffect7(() => {
|
|
899
1215
|
checkAnyPermission();
|
|
900
1216
|
}, [checkAnyPermission]);
|
|
901
|
-
return
|
|
1217
|
+
return useMemo7(() => ({
|
|
902
1218
|
hasAny,
|
|
903
1219
|
isLoading,
|
|
904
1220
|
error,
|
|
905
1221
|
refetch: checkAnyPermission
|
|
906
1222
|
}), [hasAny, isLoading, error, checkAnyPermission]);
|
|
907
1223
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const
|
|
1224
|
+
|
|
1225
|
+
// src/rbac/hooks/permissions/useMultiplePermissions.ts
|
|
1226
|
+
import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo8, useState as useState8 } from "react";
|
|
1227
|
+
function useMultiplePermissions(userId, scope, permissions, useCache = true) {
|
|
1228
|
+
const [results, setResults] = useState8({});
|
|
1229
|
+
const [isLoading, setIsLoading] = useState8(true);
|
|
1230
|
+
const [error, setError] = useState8(null);
|
|
1231
|
+
const checkPermissions = useCallback7(async () => {
|
|
913
1232
|
if (!userId || permissions.length === 0) {
|
|
914
|
-
|
|
1233
|
+
setResults({});
|
|
915
1234
|
setIsLoading(false);
|
|
916
1235
|
return;
|
|
917
1236
|
}
|
|
918
1237
|
try {
|
|
919
1238
|
setIsLoading(true);
|
|
920
1239
|
setError(null);
|
|
921
|
-
|
|
1240
|
+
const permissionResults = {};
|
|
922
1241
|
for (const permission of permissions) {
|
|
923
1242
|
const result = useCache ? await isPermittedCached({ userId, scope, permission }) : await isPermitted({ userId, scope, permission });
|
|
924
|
-
|
|
925
|
-
hasAllPermissions = false;
|
|
926
|
-
break;
|
|
927
|
-
}
|
|
1243
|
+
permissionResults[permission] = result;
|
|
928
1244
|
}
|
|
929
|
-
|
|
1245
|
+
setResults(permissionResults);
|
|
930
1246
|
} catch (err) {
|
|
931
1247
|
setError(err instanceof Error ? err : new Error("Failed to check permissions"));
|
|
932
|
-
|
|
1248
|
+
setResults({});
|
|
933
1249
|
} finally {
|
|
934
1250
|
setIsLoading(false);
|
|
935
1251
|
}
|
|
936
1252
|
}, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}, [
|
|
940
|
-
return
|
|
941
|
-
|
|
1253
|
+
useEffect8(() => {
|
|
1254
|
+
checkPermissions();
|
|
1255
|
+
}, [checkPermissions]);
|
|
1256
|
+
return useMemo8(() => ({
|
|
1257
|
+
results,
|
|
942
1258
|
isLoading,
|
|
943
1259
|
error,
|
|
944
|
-
refetch:
|
|
945
|
-
}), [
|
|
1260
|
+
refetch: checkPermissions
|
|
1261
|
+
}), [results, isLoading, error, checkPermissions]);
|
|
946
1262
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const
|
|
1263
|
+
|
|
1264
|
+
// src/rbac/hooks/permissions/usePermissions.ts
|
|
1265
|
+
import { useCallback as useCallback8, useEffect as useEffect9, useMemo as useMemo9, useRef as useRef3, useState as useState9 } from "react";
|
|
1266
|
+
function usePermissions(userId, organisationId, eventId, appId) {
|
|
1267
|
+
const [permissions, setPermissions] = useState9({});
|
|
1268
|
+
const [isLoading, setIsLoading] = useState9(true);
|
|
1269
|
+
const [error, setError] = useState9(null);
|
|
1270
|
+
const [fetchTrigger, setFetchTrigger] = useState9(0);
|
|
1271
|
+
const isFetchingRef = useRef3(false);
|
|
1272
|
+
const logger2 = getRBACLogger();
|
|
1273
|
+
const prevValuesRef = useRef3({ userId, organisationId, eventId, appId });
|
|
1274
|
+
const orgId = organisationId || "";
|
|
1275
|
+
useEffect9(() => {
|
|
1276
|
+
if (!userId) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (!orgId || orgId === null || typeof orgId === "string" && orgId.trim() === "") {
|
|
1280
|
+
const timeoutId = setTimeout(() => {
|
|
1281
|
+
setError(new Error("Organisation context is required for permission checks"));
|
|
1282
|
+
setIsLoading(false);
|
|
1283
|
+
}, 3e3);
|
|
1284
|
+
return () => clearTimeout(timeoutId);
|
|
1285
|
+
}
|
|
1286
|
+
if (error?.message === "Organisation context is required for permission checks") {
|
|
1287
|
+
setError(null);
|
|
1288
|
+
}
|
|
1289
|
+
}, [userId, organisationId, error, orgId]);
|
|
1290
|
+
useEffect9(() => {
|
|
1291
|
+
const paramsChanged = prevValuesRef.current.userId !== userId || prevValuesRef.current.organisationId !== organisationId || prevValuesRef.current.eventId !== eventId || prevValuesRef.current.appId !== appId;
|
|
1292
|
+
if (paramsChanged) {
|
|
1293
|
+
if (prevValuesRef.current.appId !== appId) {
|
|
1294
|
+
}
|
|
1295
|
+
prevValuesRef.current = { userId, organisationId, eventId, appId };
|
|
1296
|
+
setFetchTrigger((prev) => prev + 1);
|
|
1297
|
+
}
|
|
1298
|
+
}, [userId, organisationId, eventId, appId, logger2]);
|
|
1299
|
+
useEffect9(() => {
|
|
1300
|
+
const fetchPermissions = async () => {
|
|
1301
|
+
if (isFetchingRef.current) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (!userId) {
|
|
1305
|
+
setPermissions({});
|
|
1306
|
+
setIsLoading(false);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (!userId) {
|
|
1310
|
+
setPermissions({});
|
|
1311
|
+
setIsLoading(false);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (!orgId || orgId === null || typeof orgId === "string" && orgId.trim() === "") {
|
|
1315
|
+
setIsLoading(true);
|
|
1316
|
+
setError(null);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
isFetchingRef.current = true;
|
|
1321
|
+
setIsLoading(true);
|
|
1322
|
+
setError(null);
|
|
1323
|
+
const scope = {
|
|
1324
|
+
organisationId: orgId,
|
|
1325
|
+
eventId,
|
|
1326
|
+
appId
|
|
1327
|
+
};
|
|
1328
|
+
const permissionMap = await getPermissionMap({ userId, scope });
|
|
1329
|
+
const permissionCount = Object.keys(permissionMap).length;
|
|
1330
|
+
if (permissionCount === 0 && Object.keys(permissions).length > 0) {
|
|
1331
|
+
logger2.warn("[usePermissions] Permissions fetched but returned empty map", {
|
|
1332
|
+
scope: { organisationId: orgId, eventId, appId }
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
setPermissions(permissionMap);
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
logger2.error("[usePermissions] Failed to fetch permissions:", err);
|
|
1338
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch permissions"));
|
|
1339
|
+
} finally {
|
|
1340
|
+
setIsLoading(false);
|
|
1341
|
+
isFetchingRef.current = false;
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
fetchPermissions();
|
|
1345
|
+
}, [fetchTrigger, userId, organisationId, eventId, appId]);
|
|
1346
|
+
const hasPermission = useCallback8((permission) => {
|
|
1347
|
+
if (permissions["*"]) {
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
return permissions[permission] === true;
|
|
1351
|
+
}, [permissions]);
|
|
1352
|
+
const hasAnyPermission = useCallback8((permissionList) => {
|
|
1353
|
+
if (permissions["*"]) {
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
return permissionList.some((p) => permissions[p] === true);
|
|
1357
|
+
}, [permissions]);
|
|
1358
|
+
const hasAllPermissions = useCallback8((permissionList) => {
|
|
1359
|
+
if (permissions["*"]) {
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
return permissionList.every((p) => permissions[p] === true);
|
|
1363
|
+
}, [permissions]);
|
|
1364
|
+
const refetch = useCallback8(async () => {
|
|
1365
|
+
if (isFetchingRef.current) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
952
1368
|
if (!userId) {
|
|
953
1369
|
setPermissions({});
|
|
954
1370
|
setIsLoading(false);
|
|
955
1371
|
return;
|
|
956
1372
|
}
|
|
1373
|
+
if (!orgId || orgId === null || typeof orgId === "string" && orgId.trim() === "") {
|
|
1374
|
+
setIsLoading(true);
|
|
1375
|
+
setError(null);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
957
1378
|
try {
|
|
1379
|
+
isFetchingRef.current = true;
|
|
958
1380
|
setIsLoading(true);
|
|
959
1381
|
setError(null);
|
|
1382
|
+
const scope = {
|
|
1383
|
+
organisationId: orgId,
|
|
1384
|
+
eventId,
|
|
1385
|
+
appId
|
|
1386
|
+
};
|
|
960
1387
|
const permissionMap = await getPermissionMap({ userId, scope });
|
|
961
1388
|
setPermissions(permissionMap);
|
|
962
1389
|
} catch (err) {
|
|
963
|
-
|
|
1390
|
+
const logger3 = getRBACLogger();
|
|
1391
|
+
logger3.error("Failed to refetch permissions:", err);
|
|
1392
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch permissions"));
|
|
964
1393
|
} finally {
|
|
965
1394
|
setIsLoading(false);
|
|
1395
|
+
isFetchingRef.current = false;
|
|
966
1396
|
}
|
|
967
|
-
}, [userId,
|
|
968
|
-
|
|
969
|
-
fetchCachedPermissions();
|
|
970
|
-
}, [fetchCachedPermissions]);
|
|
971
|
-
useEffect2(() => {
|
|
972
|
-
fetchCachedPermissions();
|
|
973
|
-
}, [fetchCachedPermissions]);
|
|
974
|
-
return useMemo2(() => ({
|
|
1397
|
+
}, [userId, organisationId, eventId, appId]);
|
|
1398
|
+
return useMemo9(() => ({
|
|
975
1399
|
permissions,
|
|
976
1400
|
isLoading,
|
|
977
1401
|
error,
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1402
|
+
hasPermission,
|
|
1403
|
+
hasAnyPermission,
|
|
1404
|
+
hasAllPermissions,
|
|
1405
|
+
refetch
|
|
1406
|
+
}), [permissions, isLoading, error, hasPermission, hasAnyPermission, hasAllPermissions, refetch]);
|
|
981
1407
|
}
|
|
982
1408
|
|
|
983
1409
|
// src/rbac/hooks/useResourcePermissions.ts
|
|
984
|
-
import { useMemo as
|
|
1410
|
+
import { useMemo as useMemo10 } from "react";
|
|
985
1411
|
function useResourcePermissions(resource, options = {}) {
|
|
986
1412
|
const { enableRead = false, requireScope = true } = options;
|
|
987
1413
|
const { user, supabase } = useUnifiedAuth();
|
|
@@ -1009,8 +1435,12 @@ function useResourcePermissions(resource, options = {}) {
|
|
|
1009
1435
|
`create:${resource}`,
|
|
1010
1436
|
pageId,
|
|
1011
1437
|
// Pass resource name as pageId when appId is available to enable page permission checks
|
|
1012
|
-
true
|
|
1438
|
+
true,
|
|
1013
1439
|
// useCache
|
|
1440
|
+
null,
|
|
1441
|
+
// precomputedSuperAdmin - not checked yet
|
|
1442
|
+
void 0
|
|
1443
|
+
// appName
|
|
1014
1444
|
);
|
|
1015
1445
|
const { can: canUpdateResult, isLoading: updateLoading, error: updateError } = useCan(
|
|
1016
1446
|
user?.id || "",
|
|
@@ -1018,8 +1448,12 @@ function useResourcePermissions(resource, options = {}) {
|
|
|
1018
1448
|
`update:${resource}`,
|
|
1019
1449
|
pageId,
|
|
1020
1450
|
// Pass resource name as pageId when appId is available to enable page permission checks
|
|
1021
|
-
true
|
|
1451
|
+
true,
|
|
1022
1452
|
// useCache
|
|
1453
|
+
null,
|
|
1454
|
+
// precomputedSuperAdmin - not checked yet
|
|
1455
|
+
void 0
|
|
1456
|
+
// appName
|
|
1023
1457
|
);
|
|
1024
1458
|
const { can: canDeleteResult, isLoading: deleteLoading, error: deleteError } = useCan(
|
|
1025
1459
|
user?.id || "",
|
|
@@ -1027,8 +1461,12 @@ function useResourcePermissions(resource, options = {}) {
|
|
|
1027
1461
|
`delete:${resource}`,
|
|
1028
1462
|
pageId,
|
|
1029
1463
|
// Pass resource name as pageId when appId is available to enable page permission checks
|
|
1030
|
-
true
|
|
1464
|
+
true,
|
|
1031
1465
|
// useCache
|
|
1466
|
+
null,
|
|
1467
|
+
// precomputedSuperAdmin - not checked yet
|
|
1468
|
+
void 0
|
|
1469
|
+
// appName
|
|
1032
1470
|
);
|
|
1033
1471
|
const { can: canReadResult, isLoading: readLoading, error: readError } = useCan(
|
|
1034
1472
|
user?.id || "",
|
|
@@ -1036,13 +1474,17 @@ function useResourcePermissions(resource, options = {}) {
|
|
|
1036
1474
|
`read:${resource}`,
|
|
1037
1475
|
pageId,
|
|
1038
1476
|
// Pass resource name as pageId when appId is available to enable page permission checks
|
|
1039
|
-
true
|
|
1477
|
+
true,
|
|
1040
1478
|
// useCache
|
|
1479
|
+
null,
|
|
1480
|
+
// precomputedSuperAdmin - not checked yet
|
|
1481
|
+
void 0
|
|
1482
|
+
// appName
|
|
1041
1483
|
);
|
|
1042
|
-
const isLoading =
|
|
1484
|
+
const isLoading = useMemo10(() => {
|
|
1043
1485
|
return scopeLoading || createLoading || updateLoading || deleteLoading || enableRead && readLoading;
|
|
1044
1486
|
}, [scopeLoading, createLoading, updateLoading, deleteLoading, readLoading, enableRead]);
|
|
1045
|
-
const error =
|
|
1487
|
+
const error = useMemo10(() => {
|
|
1046
1488
|
if (scopeError) return scopeError;
|
|
1047
1489
|
if (createError) return createError;
|
|
1048
1490
|
if (updateError) return updateError;
|
|
@@ -1050,7 +1492,7 @@ function useResourcePermissions(resource, options = {}) {
|
|
|
1050
1492
|
if (enableRead && readError) return readError;
|
|
1051
1493
|
return null;
|
|
1052
1494
|
}, [scopeError, createError, updateError, deleteError, readError, enableRead]);
|
|
1053
|
-
return
|
|
1495
|
+
return useMemo10(() => ({
|
|
1054
1496
|
canCreate: (res) => {
|
|
1055
1497
|
if (res !== resource) {
|
|
1056
1498
|
return false;
|
|
@@ -1095,15 +1537,15 @@ function useResourcePermissions(resource, options = {}) {
|
|
|
1095
1537
|
}
|
|
1096
1538
|
|
|
1097
1539
|
// src/rbac/hooks/useRoleManagement.ts
|
|
1098
|
-
import { useState as
|
|
1540
|
+
import { useState as useState10, useCallback as useCallback9 } from "react";
|
|
1099
1541
|
function useRoleManagement() {
|
|
1100
1542
|
const { user, supabase } = useUnifiedAuth();
|
|
1101
|
-
const [isLoading, setIsLoading] =
|
|
1102
|
-
const [error, setError] =
|
|
1543
|
+
const [isLoading, setIsLoading] = useState10(false);
|
|
1544
|
+
const [error, setError] = useState10(null);
|
|
1103
1545
|
if (!supabase) {
|
|
1104
1546
|
throw new Error("useRoleManagement requires a Supabase client. Ensure UnifiedAuthProvider is configured.");
|
|
1105
1547
|
}
|
|
1106
|
-
const revokeEventAppRole =
|
|
1548
|
+
const revokeEventAppRole = useCallback9(async (params) => {
|
|
1107
1549
|
setIsLoading(true);
|
|
1108
1550
|
setError(null);
|
|
1109
1551
|
try {
|
|
@@ -1134,7 +1576,7 @@ function useRoleManagement() {
|
|
|
1134
1576
|
setIsLoading(false);
|
|
1135
1577
|
}
|
|
1136
1578
|
}, [user?.id]);
|
|
1137
|
-
const grantEventAppRole =
|
|
1579
|
+
const grantEventAppRole = useCallback9(async (params) => {
|
|
1138
1580
|
setIsLoading(true);
|
|
1139
1581
|
setError(null);
|
|
1140
1582
|
try {
|
|
@@ -1173,7 +1615,7 @@ function useRoleManagement() {
|
|
|
1173
1615
|
setIsLoading(false);
|
|
1174
1616
|
}
|
|
1175
1617
|
}, [user?.id]);
|
|
1176
|
-
const revokeRoleById =
|
|
1618
|
+
const revokeRoleById = useCallback9(async (roleId) => {
|
|
1177
1619
|
setIsLoading(true);
|
|
1178
1620
|
setError(null);
|
|
1179
1621
|
try {
|
|
@@ -1209,7 +1651,7 @@ function useRoleManagement() {
|
|
|
1209
1651
|
setIsLoading(false);
|
|
1210
1652
|
}
|
|
1211
1653
|
}, [user?.id, supabase]);
|
|
1212
|
-
const grantGlobalRole =
|
|
1654
|
+
const grantGlobalRole = useCallback9(async (params) => {
|
|
1213
1655
|
setIsLoading(true);
|
|
1214
1656
|
setError(null);
|
|
1215
1657
|
try {
|
|
@@ -1248,7 +1690,7 @@ function useRoleManagement() {
|
|
|
1248
1690
|
setIsLoading(false);
|
|
1249
1691
|
}
|
|
1250
1692
|
}, [user?.id, supabase]);
|
|
1251
|
-
const revokeGlobalRole =
|
|
1693
|
+
const revokeGlobalRole = useCallback9(async (params) => {
|
|
1252
1694
|
setIsLoading(true);
|
|
1253
1695
|
setError(null);
|
|
1254
1696
|
try {
|
|
@@ -1280,7 +1722,7 @@ function useRoleManagement() {
|
|
|
1280
1722
|
setIsLoading(false);
|
|
1281
1723
|
}
|
|
1282
1724
|
}, [user?.id, supabase]);
|
|
1283
|
-
const grantOrganisationRole =
|
|
1725
|
+
const grantOrganisationRole = useCallback9(async (params) => {
|
|
1284
1726
|
setIsLoading(true);
|
|
1285
1727
|
setError(null);
|
|
1286
1728
|
try {
|
|
@@ -1319,7 +1761,7 @@ function useRoleManagement() {
|
|
|
1319
1761
|
setIsLoading(false);
|
|
1320
1762
|
}
|
|
1321
1763
|
}, [user?.id, supabase]);
|
|
1322
|
-
const revokeOrganisationRole =
|
|
1764
|
+
const revokeOrganisationRole = useCallback9(async (params) => {
|
|
1323
1765
|
setIsLoading(true);
|
|
1324
1766
|
setError(null);
|
|
1325
1767
|
try {
|
|
@@ -1369,11 +1811,11 @@ function useRoleManagement() {
|
|
|
1369
1811
|
}
|
|
1370
1812
|
|
|
1371
1813
|
// src/rbac/hooks/useSecureSupabase.ts
|
|
1372
|
-
import { useMemo as
|
|
1814
|
+
import { useMemo as useMemo11, useRef as useRef4 } from "react";
|
|
1373
1815
|
var secureClientCache = /* @__PURE__ */ new Map();
|
|
1374
1816
|
var MAX_CACHE_SIZE = 5;
|
|
1375
1817
|
function getCacheKey(organisationId, eventId, appId, isSuperAdmin) {
|
|
1376
|
-
return `${organisationId}-${eventId || "no-event"}-${appId || "no-app"}-${isSuperAdmin ? "super" : "regular"}`;
|
|
1818
|
+
return `${organisationId || "no-org"}-${eventId || "no-event"}-${appId || "no-app"}-${isSuperAdmin ? "super" : "regular"}`;
|
|
1377
1819
|
}
|
|
1378
1820
|
function getSupabaseConfig() {
|
|
1379
1821
|
const getEnvVar = (key) => {
|
|
@@ -1405,19 +1847,20 @@ function useSecureSupabase(baseClient) {
|
|
|
1405
1847
|
selectedOrganisationId: selectedOrganisation?.id || null,
|
|
1406
1848
|
selectedEventId: selectedEvent?.event_id || null
|
|
1407
1849
|
});
|
|
1408
|
-
const prevContextRef =
|
|
1850
|
+
const prevContextRef = useRef4({
|
|
1409
1851
|
organisationId: void 0,
|
|
1410
1852
|
eventId: void 0,
|
|
1411
1853
|
appId: void 0
|
|
1412
1854
|
});
|
|
1413
|
-
return
|
|
1855
|
+
return useMemo11(() => {
|
|
1414
1856
|
if (eventLoading) {
|
|
1415
1857
|
return baseClient || authSupabase || null;
|
|
1416
1858
|
}
|
|
1417
1859
|
const organisationId = resolvedScope?.organisationId;
|
|
1418
1860
|
const eventId = resolvedScope?.eventId || selectedEvent?.event_id;
|
|
1419
1861
|
const appId = resolvedScope?.appId;
|
|
1420
|
-
|
|
1862
|
+
const canCreateSecureClient = user?.id && (isSuperAdmin || organisationId);
|
|
1863
|
+
if (canCreateSecureClient) {
|
|
1421
1864
|
prevContextRef.current = { organisationId, eventId, appId };
|
|
1422
1865
|
const cacheKey = getCacheKey(organisationId, eventId, appId, isSuperAdmin);
|
|
1423
1866
|
const cachedClient = secureClientCache.get(cacheKey);
|
|
@@ -1432,11 +1875,19 @@ function useSecureSupabase(baseClient) {
|
|
|
1432
1875
|
return baseClient || authSupabase || null;
|
|
1433
1876
|
}
|
|
1434
1877
|
try {
|
|
1435
|
-
const
|
|
1878
|
+
const effectiveOrganisationId = isSuperAdmin ? organisationId || null : organisationId;
|
|
1879
|
+
const baseForSecureClient = baseClient || authSupabase || null;
|
|
1880
|
+
const secureClient = baseForSecureClient ? fromSupabaseClient(
|
|
1881
|
+
baseForSecureClient,
|
|
1882
|
+
effectiveOrganisationId ?? null,
|
|
1883
|
+
eventId,
|
|
1884
|
+
appId,
|
|
1885
|
+
isSuperAdmin
|
|
1886
|
+
) : createSecureClient(
|
|
1436
1887
|
config.url,
|
|
1437
1888
|
config.key,
|
|
1438
|
-
|
|
1439
|
-
// organisationId is string, UUID is string alias
|
|
1889
|
+
effectiveOrganisationId,
|
|
1890
|
+
// organisationId is string | null, UUID is string alias
|
|
1440
1891
|
eventId,
|
|
1441
1892
|
appId,
|
|
1442
1893
|
// appId is string | undefined, UUID is string alias
|
|
@@ -1471,20 +1922,24 @@ function useSecureSupabase(baseClient) {
|
|
|
1471
1922
|
}
|
|
1472
1923
|
|
|
1473
1924
|
export {
|
|
1925
|
+
SECURE_CLIENT_SYMBOL,
|
|
1926
|
+
isSecureClient,
|
|
1927
|
+
warnIfInsecureClient,
|
|
1474
1928
|
SecureSupabaseClient,
|
|
1475
1929
|
createSecureClient,
|
|
1476
1930
|
fromSupabaseClient,
|
|
1931
|
+
useResolvedScope,
|
|
1477
1932
|
useRBAC,
|
|
1933
|
+
useAccessLevel,
|
|
1934
|
+
useCachedPermissions,
|
|
1478
1935
|
scopeEqual,
|
|
1479
|
-
usePermissions,
|
|
1480
1936
|
useCan,
|
|
1481
|
-
useAccessLevel,
|
|
1482
|
-
useMultiplePermissions,
|
|
1483
|
-
useHasAnyPermission,
|
|
1484
1937
|
useHasAllPermissions,
|
|
1485
|
-
|
|
1938
|
+
useHasAnyPermission,
|
|
1939
|
+
useMultiplePermissions,
|
|
1940
|
+
usePermissions,
|
|
1486
1941
|
useResourcePermissions,
|
|
1487
1942
|
useRoleManagement,
|
|
1488
1943
|
useSecureSupabase
|
|
1489
1944
|
};
|
|
1490
|
-
//# sourceMappingURL=chunk-
|
|
1945
|
+
//# sourceMappingURL=chunk-YDQHOZNA.js.map
|