@jmruthers/pace-core 0.5.76 → 0.5.78
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 +8 -0
- package/dist/{RBACService-C4udt_Zp.d.ts → AuthService-Df3IozMG.d.ts} +10 -118
- package/dist/{DataTable-ntgmhO2W.d.ts → DataTable-BE0OXZKQ.d.ts} +9 -2
- package/dist/{DataTable-4GAVPIEG.js → DataTable-ETGVF4Y5.js} +50 -13
- package/dist/{PublicLoadingSpinner-BiNER8F5.d.ts → PublicLoadingSpinner-CnUaz0vG.d.ts} +5 -2
- package/dist/{UnifiedAuthProvider-Bj6YCf7c.d.ts → UnifiedAuthProvider-B391Aqum.d.ts} +42 -45
- package/dist/{UnifiedAuthProvider-3NKDOSOK.js → UnifiedAuthProvider-P5SOJAQ6.js} +4 -5
- package/dist/{api-DDMUKIUD.js → api-KG4A2X7P.js} +9 -3
- package/dist/{audit-6TOCAMKO.js → audit-65VNHEV2.js} +2 -2
- package/dist/{chunk-K34IM5CT.js → chunk-2OGV6IRV.js} +196 -626
- package/dist/chunk-2OGV6IRV.js.map +1 -0
- package/dist/{chunk-NTNILOBC.js → chunk-5BO3MI5Y.js} +4 -4
- package/dist/{chunk-XLZ7U46Z.js → chunk-CVMVPYAL.js} +9 -60
- package/dist/chunk-CVMVPYAL.js.map +1 -0
- package/dist/{chunk-URUTVZ7N.js → chunk-FL4ZCQLD.js} +2 -2
- package/dist/{chunk-LW7MMEAQ.js → chunk-FT2M4R4F.js} +2 -2
- package/dist/{chunk-5BSLGBYI.js → chunk-JCQZ6LA7.js} +2 -8
- package/dist/{chunk-5BSLGBYI.js.map → chunk-JCQZ6LA7.js.map} +1 -1
- package/dist/{chunk-KHJS6VIA.js → chunk-LRQ6RBJC.js} +157 -112
- package/dist/chunk-LRQ6RBJC.js.map +1 -0
- package/dist/{chunk-WN6XJWOS.js → chunk-MNJXXD6C.js} +274 -743
- package/dist/chunk-MNJXXD6C.js.map +1 -0
- package/dist/{chunk-KK73ZB4E.js → chunk-PTR5PMPE.js} +153 -132
- package/dist/chunk-PTR5PMPE.js.map +1 -0
- package/dist/{chunk-B2WTCLCV.js → chunk-Q7APDV6H.js} +18 -8
- package/dist/chunk-Q7APDV6H.js.map +1 -0
- package/dist/{chunk-A4FUBC7B.js → chunk-QGVSOUJ2.js} +2 -4
- package/dist/{chunk-A4FUBC7B.js.map → chunk-QGVSOUJ2.js.map} +1 -1
- package/dist/{chunk-FGMFQSHX.js → chunk-S63MFSY6.js} +500 -551
- package/dist/chunk-S63MFSY6.js.map +1 -0
- package/dist/{chunk-AFGTSUAD.js → chunk-VSOKOFRF.js} +4 -4
- package/dist/chunk-WUXCWRL6.js +20 -0
- package/dist/chunk-WUXCWRL6.js.map +1 -0
- package/dist/{chunk-Y6TXWPJO.js → chunk-YVVGHRGI.js} +105 -31
- package/dist/chunk-YVVGHRGI.js.map +1 -0
- package/dist/{chunk-M5IWZRBT.js → chunk-ZMNXIJP4.js} +2187 -981
- package/dist/chunk-ZMNXIJP4.js.map +1 -0
- package/dist/components.d.ts +6 -6
- package/dist/components.js +14 -18
- package/dist/components.js.map +1 -1
- package/dist/{database-C3Szpi5J.d.ts → database-BXAfr2Y_.d.ts} +18 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +8 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +19 -27
- package/dist/index.js +21 -29
- package/dist/index.js.map +1 -1
- package/dist/{organisation-BtshODVF.d.ts → organisation-D6qRDtbF.d.ts} +1 -1
- package/dist/providers.d.ts +7 -21
- package/dist/providers.js +3 -10
- package/dist/rbac/index.d.ts +71 -221
- package/dist/rbac/index.js +15 -16
- package/dist/{types-CGX9Vyf5.d.ts → types-BDg1mAGG.d.ts} +36 -6
- package/dist/types.d.ts +3 -3
- package/dist/types.js +61 -18
- package/dist/types.js.map +1 -1
- package/dist/{unified-CM7T0aTK.d.ts → unified-DQ4VcT7H.d.ts} +1 -1
- package/dist/{usePublicRouteParams-B-CumWRc.d.ts → usePublicRouteParams-BlgwXweB.d.ts} +3 -3
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +52 -9
- package/dist/utils.js.map +1 -1
- package/docs/CONTENT_AUDIT_REPORT.md +253 -0
- package/docs/DOCUMENTATION_AUDIT.md +172 -0
- package/docs/README.md +142 -147
- package/docs/STYLE_GUIDE.md +37 -0
- package/docs/api/classes/ColumnFactory.md +17 -17
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +5 -5
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +35 -5
- package/docs/api/classes/RBACEngine.md +49 -20
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +4 -4
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +11 -0
- package/docs/api/interfaces/DataTableAction.md +65 -29
- package/docs/api/interfaces/DataTableColumn.md +36 -23
- package/docs/api/interfaces/DataTableProps.md +80 -38
- package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
- package/docs/api/interfaces/EmptyStateConfig.md +5 -5
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +11 -11
- package/docs/api/interfaces/NavigationContextType.md +9 -9
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +7 -7
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +16 -3
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +2 -2
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +4 -4
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +2 -2
- package/docs/api/interfaces/RouteConfig.md +2 -2
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +94 -521
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +16 -16
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +251 -269
- package/docs/api-reference/components.md +193 -0
- package/docs/api-reference/hooks.md +265 -0
- package/docs/api-reference/providers.md +6 -0
- package/docs/api-reference/types.md +6 -0
- package/docs/api-reference/utilities.md +207 -0
- package/docs/architecture/README.md +6 -0
- package/docs/{database-schema-requirements.md → architecture/database-schema-requirements.md} +6 -0
- package/docs/architecture/rbac-security-architecture.md +258 -0
- package/docs/architecture/services.md +9 -1
- package/docs/best-practices/README.md +6 -0
- package/docs/best-practices/accessibility.md +6 -0
- package/docs/{common-patterns.md → best-practices/common-patterns.md} +6 -0
- package/docs/best-practices/deployment.md +6 -0
- package/docs/best-practices/performance.md +475 -2
- package/docs/best-practices/security.md +6 -0
- package/docs/best-practices/testing.md +6 -0
- package/docs/core-concepts/authentication.md +6 -0
- package/docs/core-concepts/events.md +6 -0
- package/docs/core-concepts/organisations.md +6 -0
- package/docs/core-concepts/permissions.md +6 -0
- package/docs/core-concepts/rbac-system.md +8 -0
- package/docs/documentation-index.md +121 -182
- package/docs/{consuming-app-vite-config.md → getting-started/consuming-app-vite-config.md} +6 -0
- package/docs/getting-started/documentation-index.md +40 -0
- package/docs/getting-started/examples/README.md +878 -35
- package/docs/{faq.md → getting-started/faq.md} +7 -1
- package/docs/getting-started/installation-guide.md +6 -0
- package/docs/{quick-reference.md → getting-started/quick-reference.md} +6 -0
- package/docs/implementation-guides/app-layout.md +6 -0
- package/docs/implementation-guides/authentication.md +1021 -0
- package/docs/implementation-guides/component-styling.md +6 -0
- package/docs/implementation-guides/data-tables.md +1264 -2076
- package/docs/implementation-guides/dynamic-colors.md +6 -0
- package/docs/implementation-guides/event-theming-summary.md +6 -0
- package/docs/{file-reference-system.md → implementation-guides/file-reference-system.md} +6 -0
- package/docs/implementation-guides/file-upload-storage.md +6 -0
- package/docs/implementation-guides/forms.md +6 -0
- package/docs/implementation-guides/inactivity-tracking.md +6 -0
- package/docs/implementation-guides/navigation.md +6 -0
- package/docs/implementation-guides/organisation-security.md +6 -0
- package/docs/implementation-guides/permission-enforcement.md +6 -0
- package/docs/implementation-guides/public-pages-advanced.md +6 -0
- package/docs/implementation-guides/public-pages.md +6 -0
- package/docs/migration/MIGRATION_GUIDE.md +827 -351
- package/docs/migration/README.md +7 -1
- package/docs/migration/organisation-context-timing-fix.md +6 -0
- package/docs/migration/rbac-migration.md +44 -1
- package/docs/migration/service-architecture.md +6 -0
- package/docs/migration/v0.4.15-tailwind-scanning.md +6 -0
- package/docs/migration/v0.4.16-css-first-approach.md +6 -0
- package/docs/migration/v0.4.17-source-path-fix.md +6 -0
- package/docs/rbac/README-rbac-rls-integration.md +6 -0
- package/docs/rbac/README.md +6 -0
- package/docs/rbac/advanced-patterns.md +6 -0
- package/docs/rbac/api-reference.md +7 -1
- package/docs/rbac/breaking-changes-v3.md +222 -0
- package/docs/rbac/examples/rbac-rls-integration-example.md +6 -0
- package/docs/rbac/examples.md +6 -0
- package/docs/rbac/getting-started.md +6 -0
- package/docs/rbac/migration-guide.md +260 -0
- package/docs/rbac/quick-start.md +70 -13
- package/docs/rbac/rbac-rls-integration.md +6 -0
- package/docs/rbac/super-admin-guide.md +6 -0
- package/docs/rbac/troubleshooting.md +6 -0
- package/docs/security/README.md +6 -0
- package/docs/security/checklist.md +6 -0
- package/docs/styles/README.md +7 -1
- package/docs/{usage.md → styles/usage.md} +6 -0
- package/docs/testing/README.md +6 -0
- package/docs/{visual-testing.md → testing/visual-testing.md} +6 -0
- package/docs/troubleshooting/README.md +387 -5
- package/docs/troubleshooting/cake-page-permission-guard-issue-summary.md +6 -0
- package/docs/troubleshooting/common-issues.md +6 -0
- package/docs/troubleshooting/database-view-compatibility.md +6 -0
- package/docs/troubleshooting/organisation-context-setup.md +6 -0
- package/docs/troubleshooting/react-hooks-issue-analysis.md +6 -0
- package/docs/troubleshooting/styling-issues.md +6 -0
- package/docs/troubleshooting/tailwind-content-scanning.md +6 -0
- package/package.json +1 -1
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -1
- package/src/__tests__/helpers/test-providers.tsx +3 -53
- package/src/components/DataTable/DataTable.test.tsx +319 -0
- package/src/components/DataTable/DataTable.tsx +32 -11
- package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx → DataTable.comprehensive.test.tsx.skip} +6 -4
- package/src/components/DataTable/__tests__/{DataTable.test.tsx → DataTable.test.tsx.skip} +6 -4
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +31 -9
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +601 -0
- package/src/components/DataTable/__tests__/keyboard.test.tsx +615 -0
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +639 -0
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx.skip +330 -0
- package/src/components/DataTable/components/AccessDeniedPage.tsx +2 -2
- package/src/components/DataTable/components/ActionButtons.tsx +88 -104
- package/src/components/DataTable/components/DataTableCore.tsx +309 -337
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +4 -2
- package/src/components/DataTable/components/DataTableModals.tsx +22 -1
- package/src/components/DataTable/components/EditableRow.tsx +69 -84
- package/src/components/DataTable/components/EmptyState.tsx +5 -1
- package/src/components/DataTable/components/ImportModal.tsx +65 -36
- package/src/components/DataTable/components/PaginationControls.tsx +40 -100
- package/src/components/DataTable/components/UnifiedTableBody.tsx +125 -148
- package/src/components/DataTable/context/DataTableContext.tsx +1 -1
- package/src/components/DataTable/core/ColumnFactory.ts +5 -0
- package/src/components/DataTable/examples/HierarchicalActionsExample.tsx +12 -10
- package/src/components/DataTable/examples/HierarchicalExample.tsx +1 -1
- package/src/components/DataTable/examples/InitialPageSizeExample.tsx +1 -0
- package/src/components/DataTable/examples/PerformanceExample.tsx +1 -0
- package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +1 -5
- package/src/components/DataTable/hooks/__tests__/useColumnVisibilityPersistence.test.ts +167 -0
- package/src/components/DataTable/hooks/index.ts +7 -0
- package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +32 -15
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +102 -0
- package/src/components/DataTable/hooks/useDataTableConfiguration.ts +89 -0
- package/src/components/DataTable/hooks/useDataTableDataPipeline.ts +117 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +71 -27
- package/src/components/DataTable/hooks/useDataTableState.ts +39 -11
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +33 -0
- package/src/components/DataTable/hooks/useHierarchicalState.ts +15 -1
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +447 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +94 -0
- package/src/components/DataTable/hooks/useTableColumns.ts +10 -7
- package/src/components/DataTable/hooks/useTableHandlers.ts +174 -0
- package/src/components/DataTable/index.ts +12 -3
- package/src/components/DataTable/types.ts +129 -9
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +159 -22
- package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +111 -0
- package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +15 -29
- package/src/components/DataTable/utils/a11yUtils.ts +244 -0
- package/src/components/DataTable/utils/debugTools.ts +609 -0
- package/src/components/DataTable/utils/exportUtils.ts +114 -16
- package/src/components/DataTable/utils/flexibleImport.ts +202 -32
- package/src/components/DataTable/utils/hierarchicalUtils.ts +1 -1
- package/src/components/DataTable/utils/index.ts +2 -0
- package/src/components/DataTable/utils/paginationUtils.ts +350 -0
- package/src/components/DataTable/utils/rowUtils.ts +6 -5
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +19 -24
- package/src/components/NavigationMenu/NavigationMenu.tsx +19 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +1 -23
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +56 -6
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +137 -13
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +1 -1
- package/src/components/Select/Select.tsx +1 -0
- package/src/components/examples/PermissionExample.tsx +173 -0
- package/src/examples/CorrectPublicPageImplementation.tsx +301 -0
- package/src/examples/PublicEventPage.tsx +274 -0
- package/src/examples/PublicPageApp.tsx +308 -0
- package/src/examples/PublicPageUsageExample.tsx +216 -0
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +12 -1
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +129 -17
- package/src/hooks/__tests__/useRBAC.unit.test.ts +151 -846
- package/src/hooks/useOrganisationPermissions.test.ts +42 -18
- package/src/hooks/useOrganisationPermissions.ts +12 -6
- package/src/hooks/useOrganisationSecurity.test.ts +138 -85
- package/src/hooks/useOrganisationSecurity.ts +41 -10
- package/src/index.ts +0 -1
- package/src/providers/AuthProvider.simplified.tsx +880 -0
- package/src/providers/UnifiedAuthProvider.test.simple.tsx +8 -8
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +29 -19
- package/src/providers/index.ts +0 -1
- package/src/providers/services/EventServiceProvider.tsx +19 -15
- package/src/providers/services/InactivityServiceProvider.tsx +19 -15
- package/src/providers/services/OrganisationServiceProvider.tsx +19 -15
- package/src/providers/services/UnifiedAuthProvider.tsx +156 -127
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +1 -1
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -3
- package/src/rbac/README.md +1 -1
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +25 -27
- package/src/rbac/__tests__/auth-rbac-security.integration.test.tsx +313 -0
- package/src/rbac/__tests__/engine.comprehensive.test.ts +114 -348
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +28 -110
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +33 -85
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +2 -2
- package/src/rbac/adapters.tsx +26 -69
- package/src/rbac/api.test.ts +90 -27
- package/src/rbac/api.ts +61 -10
- package/src/rbac/audit.test.ts +33 -38
- package/src/rbac/audit.ts +21 -6
- package/src/rbac/cache.ts +33 -1
- package/src/rbac/components/NavigationGuard.tsx +11 -11
- package/src/rbac/components/NavigationProvider.test.tsx +11 -5
- package/src/rbac/components/NavigationProvider.tsx +37 -13
- package/src/rbac/components/PagePermissionGuard.tsx +111 -50
- package/src/rbac/components/PagePermissionProvider.tsx +5 -5
- package/src/rbac/components/PermissionEnforcer.tsx +11 -11
- package/src/rbac/components/RoleBasedRouter.tsx +5 -5
- package/src/rbac/components/SecureDataProvider.tsx +5 -5
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +8 -8
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +14 -14
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +12 -12
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +6 -6
- package/src/rbac/engine.test.simple.ts +19 -13
- package/src/rbac/engine.test.ts +1 -0
- package/src/rbac/engine.ts +330 -766
- package/src/rbac/errors.ts +156 -0
- package/src/rbac/hooks/usePermissions.ts +32 -10
- package/src/rbac/hooks/useRBAC.test.ts +126 -512
- package/src/rbac/hooks/useRBAC.ts +147 -193
- package/src/rbac/hooks/useResolvedScope.ts +12 -0
- package/src/rbac/index.ts +7 -4
- package/src/rbac/security.ts +109 -18
- package/src/rbac/types.ts +12 -1
- package/src/services/AuthService.ts +2 -15
- package/src/services/EventService.ts +43 -46
- package/src/services/OrganisationService.ts +51 -31
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +1 -1
- package/src/services/__tests__/OrganisationService.test.ts +1 -1
- package/src/services/base/BaseService.ts +8 -0
- package/src/styles/base.css +208 -0
- package/src/styles/semantic.css +24 -0
- package/src/types/database.generated.ts +7347 -0
- package/src/types/database.ts +20 -0
- package/src/utils/logger.ts +179 -0
- package/src/utils/organisationContext.ts +11 -4
- package/src/utils/storage/__tests__/helpers.unit.test.ts +6 -2
- package/dist/appNameResolver-UURKN7NF.js +0 -22
- package/dist/audit-6TOCAMKO.js.map +0 -1
- package/dist/chunk-B2WTCLCV.js.map +0 -1
- package/dist/chunk-FGMFQSHX.js.map +0 -1
- package/dist/chunk-K34IM5CT.js.map +0 -1
- package/dist/chunk-KHJS6VIA.js.map +0 -1
- package/dist/chunk-KK73ZB4E.js.map +0 -1
- package/dist/chunk-M5IWZRBT.js.map +0 -1
- package/dist/chunk-ULBI5JGB.js +0 -109
- package/dist/chunk-ULBI5JGB.js.map +0 -1
- package/dist/chunk-WN6XJWOS.js.map +0 -1
- package/dist/chunk-XLZ7U46Z.js.map +0 -1
- package/dist/chunk-Y6TXWPJO.js.map +0 -1
- package/docs/DOCUMENTATION_CHECKLIST.md +0 -281
- package/docs/TERMINOLOGY.md +0 -231
- package/docs/api/interfaces/RBACContextType.md +0 -468
- package/docs/api/interfaces/RBACProviderProps.md +0 -107
- package/docs/best-practices/performance-expansion.md +0 -473
- package/docs/breaking-changes.md +0 -179
- package/docs/consuming-app-example.md +0 -290
- package/docs/documentation-templates.md +0 -539
- package/docs/examples/navigation-menu-auth-fix.md +0 -344
- package/docs/getting-started/examples/basic-auth-app.md +0 -520
- package/docs/getting-started/examples/full-featured-app.md +0 -616
- package/docs/getting-started/quick-start.md +0 -376
- package/docs/implementation-guides/datatable-filtering.md +0 -313
- package/docs/implementation-guides/datatable-rbac-usage.md +0 -317
- package/docs/implementation-guides/hierarchical-datatable.md +0 -850
- package/docs/implementation-guides/large-datasets.md +0 -281
- package/docs/implementation-guides/performance.md +0 -403
- package/docs/migration/quick-migration-guide.md +0 -320
- package/docs/migration-guide.md +0 -193
- package/docs/migration-guides/unified-auth-provider-mandatory-timeouts.md +0 -226
- package/docs/performance/README.md +0 -551
- package/docs/style-guide.md +0 -964
- package/docs/troubleshooting/authentication-issues.md +0 -334
- package/docs/troubleshooting/debugging.md +0 -1117
- package/docs/troubleshooting/migration.md +0 -918
- package/src/__tests__/hooks/usePermissions.test.ts +0 -261
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +0 -574
- package/src/hooks/__tests__/ServiceHooks.test.tsx +0 -613
- package/src/hooks/services/__tests__/useServiceHooks.test.tsx +0 -137
- package/src/hooks/services/usePermissions.ts +0 -70
- package/src/hooks/services/useRBACService.ts +0 -30
- package/src/hooks/usePermissionCheck.ts +0 -150
- package/src/providers/__tests__/ServiceProviders.test.tsx +0 -477
- package/src/providers/services/RBACServiceProvider.tsx +0 -79
- package/src/rbac/__tests__/integration.authflow.test.tsx +0 -119
- package/src/rbac/__tests__/integration.navigation.test.tsx +0 -69
- package/src/rbac/__tests__/integration.securedata.test.tsx +0 -92
- package/src/rbac/__tests__/integration.smoke.test.tsx +0 -73
- package/src/rbac/providers/RBACProvider.tsx +0 -645
- package/src/rbac/providers/__tests__/RBACProvider.integration.test.tsx +0 -688
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +0 -1186
- package/src/rbac/providers/index.ts +0 -11
- package/src/services/RBACService.ts +0 -522
- package/src/services/__tests__/RBACService.test.ts +0 -492
- package/src/services/interfaces/IRBACService.ts +0 -62
- package/src/utils/appNameResolver.test 2.ts +0 -494
- /package/dist/{DataTable-4GAVPIEG.js.map → DataTable-ETGVF4Y5.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-3NKDOSOK.js.map → UnifiedAuthProvider-P5SOJAQ6.js.map} +0 -0
- /package/dist/{api-DDMUKIUD.js.map → api-KG4A2X7P.js.map} +0 -0
- /package/dist/{appNameResolver-UURKN7NF.js.map → audit-65VNHEV2.js.map} +0 -0
- /package/dist/{chunk-NTNILOBC.js.map → chunk-5BO3MI5Y.js.map} +0 -0
- /package/dist/{chunk-URUTVZ7N.js.map → chunk-FL4ZCQLD.js.map} +0 -0
- /package/dist/{chunk-LW7MMEAQ.js.map → chunk-FT2M4R4F.js.map} +0 -0
- /package/dist/{chunk-AFGTSUAD.js.map → chunk-VSOKOFRF.js.map} +0 -0
- /package/docs/{app.css.example → styles/app.css.example} +0 -0
package/src/rbac/engine.ts
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RBAC Core Engine
|
|
2
|
+
* RBAC Core Engine - Simplified Version
|
|
3
3
|
* @package @jmruthers/pace-core
|
|
4
4
|
* @module RBAC/Engine
|
|
5
|
-
* @since
|
|
5
|
+
* @since 2.0.0
|
|
6
6
|
*
|
|
7
|
-
* This
|
|
7
|
+
* This is a drastically simplified version that delegates permission checking to a single RPC function.
|
|
8
|
+
* All the complex grant collection logic has been moved to the database for better performance and security.
|
|
9
|
+
*
|
|
10
|
+
* BREAKING CHANGES FROM v1:
|
|
11
|
+
* - No more client-side grant collection
|
|
12
|
+
* - No more complex permission resolution algorithm
|
|
13
|
+
* - Single RPC call for all permission checks
|
|
14
|
+
* - Caching is still supported for performance
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
11
18
|
import { Database } from '../types/database';
|
|
12
|
-
import {
|
|
13
|
-
UUID,
|
|
14
|
-
Permission,
|
|
15
|
-
Scope,
|
|
16
|
-
PermissionCheck,
|
|
17
|
-
AccessLevel,
|
|
19
|
+
import {
|
|
20
|
+
UUID,
|
|
21
|
+
Permission,
|
|
22
|
+
Scope,
|
|
23
|
+
PermissionCheck,
|
|
24
|
+
AccessLevel,
|
|
18
25
|
PermissionMap,
|
|
19
26
|
Operation,
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
RBACAppContext,
|
|
28
|
+
RBACRoleContext,
|
|
29
|
+
RBACPermission,
|
|
22
30
|
} from './types';
|
|
23
31
|
import { rbacCache, RBACCache } from './cache';
|
|
24
32
|
import { emitAuditEvent } from './audit';
|
|
25
33
|
import { initializeCacheInvalidation } from './cache-invalidation';
|
|
26
|
-
import {
|
|
34
|
+
import { categorizeError, mapErrorCategoryToSecurityEventType } from './errors';
|
|
27
35
|
import {
|
|
28
36
|
RBACSecurityValidator,
|
|
29
37
|
RBACSecurityMiddleware,
|
|
@@ -32,19 +40,10 @@ import {
|
|
|
32
40
|
} from './security';
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
*/
|
|
37
|
-
interface Grant {
|
|
38
|
-
type: 'allow' | 'deny';
|
|
39
|
-
permission: Permission;
|
|
40
|
-
scope: 'global' | 'organisation' | 'eventApp' | 'page';
|
|
41
|
-
source: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* RBAC Engine
|
|
43
|
+
* Simplified RBAC Engine
|
|
46
44
|
*
|
|
47
|
-
*
|
|
45
|
+
* Delegates all permission checks to the database via a single RPC function.
|
|
46
|
+
* This reduces complexity, improves performance, and enhances security.
|
|
48
47
|
*/
|
|
49
48
|
export class RBACEngine {
|
|
50
49
|
private supabase: SupabaseClient<Database>;
|
|
@@ -61,78 +60,99 @@ export class RBACEngine {
|
|
|
61
60
|
/**
|
|
62
61
|
* Check if a user has a specific permission
|
|
63
62
|
*
|
|
63
|
+
* This method now delegates to the database RPC function for all the heavy lifting.
|
|
64
|
+
*
|
|
64
65
|
* @param input - Permission check input
|
|
65
|
-
* @param securityContext -
|
|
66
|
+
* @param securityContext - Security context for validation (required)
|
|
66
67
|
* @returns Promise resolving to permission result
|
|
67
68
|
*/
|
|
68
|
-
async isPermitted(input: PermissionCheck, securityContext
|
|
69
|
+
async isPermitted(input: PermissionCheck, securityContext: SecurityContext): Promise<boolean> {
|
|
69
70
|
const startTime = Date.now();
|
|
70
71
|
const { userId, permission, scope, pageId } = input;
|
|
71
72
|
|
|
72
73
|
// Track cache usage for audit
|
|
73
74
|
let cacheHit = false;
|
|
74
|
-
let cacheSource: 'memory' | '
|
|
75
|
+
let cacheSource: 'memory' | 'rpc' = 'rpc';
|
|
75
76
|
|
|
76
77
|
try {
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
// ========================================================================
|
|
79
|
+
// STEP 1: Security Validation & Rate Limiting (MANDATORY)
|
|
80
|
+
// ========================================================================
|
|
81
|
+
|
|
82
|
+
// Validate input
|
|
83
|
+
const validation = await this.securityMiddleware.validateInput(input, securityContext);
|
|
84
|
+
if (!validation.isValid) {
|
|
85
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
86
|
+
type: 'invalid_input',
|
|
87
|
+
userId,
|
|
88
|
+
details: { errors: validation.errors, input: JSON.stringify(input) },
|
|
89
|
+
});
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
// Check rate limits
|
|
94
|
+
const rateLimit = await this.securityMiddleware.checkRateLimit(securityContext);
|
|
95
|
+
if (!rateLimit.isAllowed) {
|
|
96
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
97
|
+
type: 'rate_limit_exceeded',
|
|
98
|
+
userId,
|
|
99
|
+
details: { remaining: rateLimit.remaining },
|
|
100
|
+
});
|
|
101
|
+
return false;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
//
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
104
|
+
// Validate user ID format
|
|
105
|
+
if (!RBACSecurityValidator.validateUserId(userId)) {
|
|
106
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
107
|
+
type: 'invalid_input',
|
|
108
|
+
userId,
|
|
109
|
+
details: { error: 'Invalid user ID format' },
|
|
110
|
+
});
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
110
113
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
// Validate permission format
|
|
115
|
+
if (!RBACSecurityValidator.validatePermission(permission)) {
|
|
116
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
117
|
+
type: 'invalid_input',
|
|
118
|
+
userId,
|
|
119
|
+
details: { error: 'Invalid permission format', permission },
|
|
120
|
+
});
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
119
123
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
// Validate scope format
|
|
125
|
+
if (!RBACSecurityValidator.validateScope(scope)) {
|
|
126
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
127
|
+
type: 'invalid_input',
|
|
128
|
+
userId,
|
|
129
|
+
details: { error: 'Invalid scope format', scope },
|
|
130
|
+
});
|
|
131
|
+
return false;
|
|
128
132
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
// ========================================================================
|
|
135
|
+
// STEP 2: Check Cache (OPTIONAL - for performance)
|
|
136
|
+
// ========================================================================
|
|
137
|
+
|
|
138
|
+
const cacheKey = RBACCache.generateKey(
|
|
139
|
+
userId,
|
|
140
|
+
permission,
|
|
141
|
+
scope.organisationId,
|
|
142
|
+
scope.eventId,
|
|
143
|
+
scope.appId,
|
|
144
|
+
pageId
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const cached = rbacCache.get<boolean>(cacheKey);
|
|
148
|
+
if (cached !== null) {
|
|
149
|
+
cacheHit = true;
|
|
150
|
+
cacheSource = 'memory';
|
|
151
|
+
|
|
132
152
|
const duration = Date.now() - startTime;
|
|
133
|
-
|
|
153
|
+
|
|
154
|
+
// Audit cache hit (if organisation context exists)
|
|
134
155
|
if (scope.organisationId) {
|
|
135
|
-
// Resolve pageId to UUID if it's a page name
|
|
136
156
|
const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
|
|
137
157
|
await emitAuditEvent({
|
|
138
158
|
type: 'permission_check',
|
|
@@ -142,159 +162,111 @@ export class RBACEngine {
|
|
|
142
162
|
appId: scope.appId,
|
|
143
163
|
pageId: resolvedPageId,
|
|
144
164
|
permission,
|
|
145
|
-
decision:
|
|
165
|
+
decision: cached,
|
|
146
166
|
source: 'api',
|
|
147
|
-
bypass: true,
|
|
148
167
|
duration_ms: duration,
|
|
168
|
+
cache_hit: true,
|
|
169
|
+
cache_source: 'memory',
|
|
149
170
|
});
|
|
150
171
|
}
|
|
151
|
-
|
|
172
|
+
|
|
173
|
+
return cached;
|
|
152
174
|
}
|
|
153
175
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
176
|
+
// ========================================================================
|
|
177
|
+
// STEP 3: Call Simplified RPC Function (SINGLE DATABASE CALL)
|
|
178
|
+
// ========================================================================
|
|
179
|
+
|
|
180
|
+
// This single RPC call replaces hundreds of lines of complex client-side logic:
|
|
181
|
+
// - No more super admin checks here (RPC handles it)
|
|
182
|
+
// - No more grant collection (RPC handles it)
|
|
183
|
+
// - No more permission matching (RPC handles it)
|
|
184
|
+
// - No more deny-override-allow logic (RPC handles it)
|
|
185
|
+
|
|
186
|
+
const { data, error } = await (this.supabase as any).rpc('rbac_check_permission_simplified', {
|
|
187
|
+
p_user_id: userId,
|
|
188
|
+
p_permission: permission,
|
|
189
|
+
p_organisation_id: scope.organisationId || undefined,
|
|
190
|
+
p_event_id: scope.eventId || undefined,
|
|
191
|
+
p_app_id: scope.appId || undefined,
|
|
192
|
+
p_page_id: pageId || undefined,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (error) {
|
|
196
|
+
console.error('[RBACEngine] RPC error:', error);
|
|
179
197
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
198
|
+
const category = categorizeError(error);
|
|
199
|
+
const eventType = mapErrorCategoryToSecurityEventType(category);
|
|
200
|
+
const errorDetails = error as { message?: string; code?: string; hint?: string; details?: string };
|
|
183
201
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
RBACSecurityValidator.logSecurityEvent({
|
|
203
|
+
type: eventType,
|
|
204
|
+
userId,
|
|
205
|
+
details: {
|
|
206
|
+
error: errorDetails?.message || 'RPC call failed',
|
|
207
|
+
code: errorDetails?.code,
|
|
208
|
+
hint: errorDetails?.hint,
|
|
209
|
+
details: errorDetails?.details,
|
|
210
|
+
permission,
|
|
211
|
+
scope: JSON.stringify(scope),
|
|
212
|
+
category,
|
|
213
|
+
},
|
|
193
214
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Only emit audit event if we have a valid organisation ID
|
|
198
|
-
if (scope.organisationId) {
|
|
199
|
-
// Resolve pageId to UUID if it's a page name
|
|
200
|
-
const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
|
|
201
|
-
await emitAuditEvent({
|
|
202
|
-
type: 'permission_denied',
|
|
203
|
-
userId,
|
|
204
|
-
organisationId: scope.organisationId,
|
|
205
|
-
eventId: scope.eventId,
|
|
206
|
-
appId: scope.appId,
|
|
207
|
-
pageId: resolvedPageId,
|
|
208
|
-
permission,
|
|
209
|
-
source: 'api',
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
215
|
+
|
|
216
|
+
// Fail securely - deny on error
|
|
217
|
+
return false;
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
|
|
217
|
-
const allows = grants.filter(g => g.type === 'allow');
|
|
218
|
-
console.log('[RBACEngine] Allow grants:', allows);
|
|
219
|
-
let hasPermission = false;
|
|
220
|
+
const hasPermission = data === true;
|
|
220
221
|
|
|
221
|
-
//
|
|
222
|
-
|
|
222
|
+
// ========================================================================
|
|
223
|
+
// STEP 4: Cache Result & Audit (COMPLETION)
|
|
224
|
+
// ========================================================================
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
console.log(`[RBACEngine] Checking ${scopeType} allows:`, scopeAllows);
|
|
227
|
-
for (const allow of scopeAllows) {
|
|
228
|
-
console.log(`[RBACEngine] About to check permission match for ${scopeType}:`, {
|
|
229
|
-
allowPermission: allow.permission,
|
|
230
|
-
requestedPermission: permission,
|
|
231
|
-
scopeType
|
|
232
|
-
});
|
|
233
|
-
const matches = this.permissionMatches(allow.permission, permission);
|
|
234
|
-
console.log(`[RBACEngine] Permission match result:`, {
|
|
235
|
-
scopeType,
|
|
236
|
-
allowPermission: allow.permission,
|
|
237
|
-
requestedPermission: permission,
|
|
238
|
-
matches
|
|
239
|
-
});
|
|
240
|
-
if (matches) {
|
|
241
|
-
console.log(`[RBACEngine] Permission GRANTED by ${scopeType} allow rule`);
|
|
242
|
-
hasPermission = true;
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
if (hasPermission) break;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 6) Return final decision; emit audit event
|
|
250
|
-
const finalDecision = hasPermission;
|
|
251
|
-
const _duration = Date.now() - startTime;
|
|
226
|
+
// Cache the result for 60 seconds
|
|
227
|
+
rbacCache.set(cacheKey, hasPermission, 60000);
|
|
252
228
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
pageId,
|
|
257
|
-
hasPermission,
|
|
258
|
-
grantsCount: grants.length,
|
|
259
|
-
allowsCount: allows.length,
|
|
260
|
-
deniesCount: denies.length,
|
|
261
|
-
duration: _duration
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Only emit audit event if we have a valid organisation ID
|
|
229
|
+
const duration = Date.now() - startTime;
|
|
230
|
+
|
|
231
|
+
// Emit audit event (if organisation context exists)
|
|
265
232
|
if (scope.organisationId) {
|
|
266
|
-
// Resolve pageId to UUID if it's a page name
|
|
267
233
|
const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
|
|
268
234
|
await emitAuditEvent({
|
|
269
|
-
type: 'permission_check',
|
|
235
|
+
type: hasPermission ? 'permission_check' : 'permission_denied',
|
|
270
236
|
userId,
|
|
271
237
|
organisationId: scope.organisationId,
|
|
272
238
|
eventId: scope.eventId,
|
|
273
239
|
appId: scope.appId,
|
|
274
240
|
pageId: resolvedPageId,
|
|
275
241
|
permission,
|
|
276
|
-
decision:
|
|
242
|
+
decision: hasPermission,
|
|
277
243
|
source: 'api',
|
|
278
|
-
duration_ms:
|
|
244
|
+
duration_ms: duration,
|
|
279
245
|
cache_hit: cacheHit,
|
|
280
246
|
cache_source: cacheSource,
|
|
281
247
|
});
|
|
282
248
|
}
|
|
283
249
|
|
|
284
|
-
return
|
|
250
|
+
return hasPermission;
|
|
251
|
+
|
|
285
252
|
} catch (error) {
|
|
286
|
-
|
|
253
|
+
const category = categorizeError(error);
|
|
254
|
+
const eventType = mapErrorCategoryToSecurityEventType(category);
|
|
255
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
256
|
+
|
|
287
257
|
RBACSecurityValidator.logSecurityEvent({
|
|
288
|
-
type:
|
|
258
|
+
type: eventType,
|
|
289
259
|
userId,
|
|
290
|
-
details: {
|
|
291
|
-
error:
|
|
260
|
+
details: {
|
|
261
|
+
error: errorMessage,
|
|
292
262
|
permission,
|
|
293
|
-
scope: JSON.stringify(scope)
|
|
263
|
+
scope: JSON.stringify(scope),
|
|
264
|
+
category,
|
|
294
265
|
},
|
|
295
266
|
});
|
|
296
267
|
|
|
297
|
-
//
|
|
268
|
+
// Fail securely - deny access on error
|
|
269
|
+
console.error('[RBACEngine] Permission check failed:', error);
|
|
298
270
|
return false;
|
|
299
271
|
}
|
|
300
272
|
}
|
|
@@ -302,30 +274,20 @@ export class RBACEngine {
|
|
|
302
274
|
/**
|
|
303
275
|
* Get user's access level in a scope
|
|
304
276
|
*
|
|
277
|
+
* This is derived from roles, not permissions.
|
|
278
|
+
*
|
|
305
279
|
* @param input - Access level input
|
|
306
280
|
* @returns Promise resolving to access level
|
|
307
281
|
*/
|
|
308
282
|
async getAccessLevel(input: { userId: UUID; scope: Scope }): Promise<AccessLevel> {
|
|
309
283
|
const { userId, scope } = input;
|
|
310
284
|
|
|
311
|
-
// Check super admin first - super admins don't need context validation
|
|
312
|
-
const isSuperAdmin = await this.checkSuperAdmin(userId);
|
|
313
|
-
if (isSuperAdmin) {
|
|
314
|
-
return 'super';
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Validate context requirements based on app configuration
|
|
318
|
-
const validatedScope = await this.validateContextRequirements(scope, scope.appId);
|
|
319
|
-
if (!validatedScope) {
|
|
320
|
-
return 'viewer'; // Default to lowest access level for invalid context
|
|
321
|
-
}
|
|
322
|
-
|
|
323
285
|
// Check cache first
|
|
324
286
|
const cacheKey = RBACCache.generateAccessLevelKey(
|
|
325
287
|
userId,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
288
|
+
scope.organisationId || '',
|
|
289
|
+
scope.eventId,
|
|
290
|
+
scope.appId
|
|
329
291
|
);
|
|
330
292
|
|
|
331
293
|
const cached = rbacCache.get<AccessLevel>(cacheKey);
|
|
@@ -333,63 +295,97 @@ export class RBACEngine {
|
|
|
333
295
|
return cached;
|
|
334
296
|
}
|
|
335
297
|
|
|
298
|
+
// Check super admin
|
|
299
|
+
const isSuperAdmin = await this.checkSuperAdmin(userId);
|
|
300
|
+
if (isSuperAdmin) {
|
|
301
|
+
rbacCache.set(cacheKey, 'super', 60000);
|
|
302
|
+
return 'super';
|
|
303
|
+
}
|
|
304
|
+
|
|
336
305
|
// Check organisation role
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
306
|
+
if (scope.organisationId) {
|
|
307
|
+
const { data: orgRole } = await this.supabase
|
|
308
|
+
.from('rbac_organisation_roles')
|
|
309
|
+
.select('role')
|
|
310
|
+
.eq('user_id', userId)
|
|
311
|
+
.eq('organisation_id', scope.organisationId)
|
|
312
|
+
.eq('status', 'active')
|
|
313
|
+
.is('revoked_at', null)
|
|
314
|
+
.single() as { data: { role: string } | null; error: any };
|
|
315
|
+
|
|
316
|
+
if (orgRole?.role === 'org_admin') {
|
|
317
|
+
rbacCache.set(cacheKey, 'admin', 60000);
|
|
318
|
+
return 'admin';
|
|
319
|
+
}
|
|
341
320
|
}
|
|
342
321
|
|
|
343
322
|
// Check event-app role
|
|
344
|
-
if (
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
323
|
+
if (scope.eventId && scope.appId) {
|
|
324
|
+
const now = new Date().toISOString();
|
|
325
|
+
const { data: eventRole } = await this.supabase
|
|
326
|
+
.from('rbac_event_app_roles')
|
|
327
|
+
.select('role')
|
|
328
|
+
.eq('user_id', userId)
|
|
329
|
+
.eq('event_id', scope.eventId)
|
|
330
|
+
.eq('app_id', scope.appId)
|
|
331
|
+
.eq('status', 'active')
|
|
332
|
+
.lte('valid_from', now)
|
|
333
|
+
.or(`valid_to.is.null,valid_to.gte.${now}`)
|
|
334
|
+
.single() as { data: { role: string } | null; error: any };
|
|
335
|
+
|
|
336
|
+
if (eventRole?.role === 'event_admin') {
|
|
337
|
+
rbacCache.set(cacheKey, 'admin', 60000);
|
|
348
338
|
return 'admin';
|
|
349
339
|
}
|
|
350
|
-
if (eventRole === 'planner') {
|
|
351
|
-
rbacCache.set(cacheKey, 'planner');
|
|
340
|
+
if (eventRole?.role === 'planner') {
|
|
341
|
+
rbacCache.set(cacheKey, 'planner', 60000);
|
|
352
342
|
return 'planner';
|
|
353
343
|
}
|
|
354
|
-
if (eventRole === 'participant') {
|
|
355
|
-
rbacCache.set(cacheKey, 'participant');
|
|
344
|
+
if (eventRole?.role === 'participant') {
|
|
345
|
+
rbacCache.set(cacheKey, 'participant', 60000);
|
|
356
346
|
return 'participant';
|
|
357
347
|
}
|
|
358
348
|
}
|
|
359
349
|
|
|
360
350
|
// Default to viewer
|
|
361
|
-
rbacCache.set(cacheKey, 'viewer');
|
|
351
|
+
rbacCache.set(cacheKey, 'viewer', 60000);
|
|
362
352
|
return 'viewer';
|
|
363
353
|
}
|
|
364
354
|
|
|
365
355
|
/**
|
|
366
356
|
* Get user's permission map for a scope
|
|
367
357
|
*
|
|
358
|
+
* This builds a map of page IDs to allowed operations.
|
|
359
|
+
* Uses the simplified RPC for each permission check.
|
|
360
|
+
*
|
|
368
361
|
* @param input - Permission map input
|
|
369
362
|
* @returns Promise resolving to permission map
|
|
370
363
|
*/
|
|
371
364
|
async getPermissionMap(input: { userId: UUID; scope: Scope }): Promise<PermissionMap> {
|
|
372
365
|
const { userId, scope } = input;
|
|
373
366
|
|
|
367
|
+
// Generate cache key early so it's available for super admin caching
|
|
368
|
+
const cacheKey = RBACCache.generatePermissionMapKey(
|
|
369
|
+
userId,
|
|
370
|
+
scope.organisationId || '',
|
|
371
|
+
scope.eventId,
|
|
372
|
+
scope.appId
|
|
373
|
+
);
|
|
374
|
+
|
|
374
375
|
// Check super admin first - super admins have all permissions
|
|
375
376
|
const isSuperAdmin = await this.checkSuperAdmin(userId);
|
|
376
377
|
if (isSuperAdmin) {
|
|
377
|
-
|
|
378
|
+
const wildcardMap: PermissionMap = { '*': true };
|
|
379
|
+
rbacCache.set(cacheKey, wildcardMap, 60000);
|
|
380
|
+
return wildcardMap;
|
|
378
381
|
}
|
|
379
382
|
|
|
380
|
-
// Validate
|
|
381
|
-
|
|
382
|
-
if (!validatedScope) {
|
|
383
|
+
// Validate scope
|
|
384
|
+
if (!scope.organisationId) {
|
|
383
385
|
return {}; // No permissions without valid context
|
|
384
386
|
}
|
|
385
387
|
|
|
386
388
|
// Check cache first
|
|
387
|
-
const cacheKey = RBACCache.generatePermissionMapKey(
|
|
388
|
-
userId,
|
|
389
|
-
validatedScope.organisationId!,
|
|
390
|
-
validatedScope.eventId,
|
|
391
|
-
validatedScope.appId
|
|
392
|
-
);
|
|
393
389
|
|
|
394
390
|
const cached = rbacCache.get<PermissionMap>(cacheKey);
|
|
395
391
|
if (cached) {
|
|
@@ -399,584 +395,152 @@ export class RBACEngine {
|
|
|
399
395
|
const permissionMap: PermissionMap = {};
|
|
400
396
|
|
|
401
397
|
// Get all pages for the app
|
|
402
|
-
if (
|
|
398
|
+
if (scope.appId) {
|
|
403
399
|
const { data: pages } = await this.supabase
|
|
404
400
|
.from('rbac_app_pages')
|
|
405
401
|
.select('id, page_name')
|
|
406
|
-
.eq('app_id',
|
|
402
|
+
.eq('app_id', scope.appId) as { data: Array<{ id: string; page_name: string }> | null };
|
|
407
403
|
|
|
408
404
|
if (pages) {
|
|
405
|
+
// Create a security context for permission checks
|
|
406
|
+
const securityContext: SecurityContext = {
|
|
407
|
+
userId,
|
|
408
|
+
organisationId: scope.organisationId,
|
|
409
|
+
timestamp: new Date(),
|
|
410
|
+
};
|
|
411
|
+
|
|
409
412
|
for (const page of pages) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// Check each CRUD operation (read, create, update, delete only)
|
|
413
|
+
// Check each CRUD operation
|
|
413
414
|
for (const operation of ['read', 'create', 'update', 'delete'] as Operation[]) {
|
|
414
|
-
const hasPermission = await this.isPermitted(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
415
|
+
const hasPermission = await this.isPermitted(
|
|
416
|
+
{
|
|
417
|
+
userId,
|
|
418
|
+
scope,
|
|
419
|
+
permission: `${operation}:${page.page_name}`,
|
|
420
|
+
pageId: page.id,
|
|
421
|
+
},
|
|
422
|
+
securityContext
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const permissionKey = `${operation}:${page.page_name}` as Permission;
|
|
426
|
+
permissionMap[permissionKey] = hasPermission;
|
|
424
427
|
}
|
|
425
|
-
|
|
426
|
-
permissionMap[page.id] = operations;
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
rbacCache.set(cacheKey, permissionMap);
|
|
432
|
+
rbacCache.set(cacheKey, permissionMap, 60000);
|
|
432
433
|
return permissionMap;
|
|
433
434
|
}
|
|
434
435
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
* @param userId - User ID
|
|
443
|
-
* @returns Promise resolving to super admin status
|
|
444
|
-
*/
|
|
445
|
-
private async checkSuperAdmin(userId: UUID): Promise<boolean> {
|
|
446
|
-
// Check cache first
|
|
447
|
-
const cacheKey = `super_admin:${userId}`;
|
|
448
|
-
const cached = rbacCache.get<boolean>(cacheKey);
|
|
449
|
-
if (cached !== null) {
|
|
450
|
-
return cached;
|
|
451
|
-
}
|
|
436
|
+
async resolveAppContext(input: { userId: UUID; appName: string }): Promise<RBACAppContext | null> {
|
|
437
|
+
try {
|
|
438
|
+
const { userId, appName } = input;
|
|
439
|
+
const { data, error } = await (this.supabase as any).rpc('util_app_resolve', {
|
|
440
|
+
p_user_id: userId,
|
|
441
|
+
p_app_name: appName,
|
|
442
|
+
});
|
|
452
443
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
.from('rbac_global_roles')
|
|
458
|
-
.select('role')
|
|
459
|
-
.eq('user_id', userId)
|
|
460
|
-
.eq('role', 'super_admin')
|
|
461
|
-
.lte('valid_from', now)
|
|
462
|
-
.or(`valid_to.is.null,valid_to.gte.${now}`)
|
|
463
|
-
.limit(1) as { data: Array<{ role: string }> | null; error: any };
|
|
444
|
+
if (error) {
|
|
445
|
+
console.error('[RBACEngine] Failed to resolve app context:', error);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
464
448
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
rbacCache.set(cacheKey, isSuperAdmin, 60000);
|
|
469
|
-
|
|
470
|
-
return Boolean(isSuperAdmin);
|
|
471
|
-
}
|
|
449
|
+
if (!data || data.length === 0) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
472
452
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
* @returns Promise resolving to app configuration
|
|
478
|
-
*/
|
|
479
|
-
async getAppConfig(appId: UUID): Promise<{ requires_event: boolean } | null> {
|
|
480
|
-
const { data, error } = await this.supabase
|
|
481
|
-
.from('rbac_apps')
|
|
482
|
-
.select('requires_event')
|
|
483
|
-
.eq('id', appId)
|
|
484
|
-
.eq('is_active', true)
|
|
485
|
-
.single() as { data: { requires_event: boolean } | null; error: any };
|
|
453
|
+
const appData = data[0] as { app_id: UUID; has_access: boolean };
|
|
454
|
+
if (!appData?.app_id) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
486
457
|
|
|
487
|
-
|
|
458
|
+
return {
|
|
459
|
+
appId: appData.app_id,
|
|
460
|
+
hasAccess: appData.has_access !== false,
|
|
461
|
+
};
|
|
462
|
+
} catch (error) {
|
|
463
|
+
console.error('[RBACEngine] Unexpected error resolving app context:', error);
|
|
488
464
|
return null;
|
|
489
465
|
}
|
|
490
|
-
|
|
491
|
-
return { requires_event: data.requires_event };
|
|
492
466
|
}
|
|
493
467
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
private async resolveOrganisationFromEvent(eventId: string): Promise<UUID | null> {
|
|
501
|
-
const { data, error } = await this.supabase
|
|
502
|
-
.from('event')
|
|
503
|
-
.select('organisation_id')
|
|
504
|
-
.eq('id', eventId)
|
|
505
|
-
.single() as { data: { organisation_id: string } | null; error: any };
|
|
468
|
+
async getRoleContext(input: { userId: UUID; scope: Scope }): Promise<RBACRoleContext> {
|
|
469
|
+
const result: RBACRoleContext = {
|
|
470
|
+
globalRole: null,
|
|
471
|
+
organisationRole: null,
|
|
472
|
+
eventAppRole: null,
|
|
473
|
+
};
|
|
506
474
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
475
|
+
try {
|
|
476
|
+
const { userId, scope } = input;
|
|
477
|
+
const { data, error } = await (this.supabase as any).rpc('rbac_permissions_get', {
|
|
478
|
+
p_user_id: userId,
|
|
479
|
+
p_organisation_id: scope.organisationId || null,
|
|
480
|
+
p_event_id: scope.eventId || null,
|
|
481
|
+
p_app_id: scope.appId || null,
|
|
482
|
+
});
|
|
510
483
|
|
|
511
|
-
|
|
512
|
-
|
|
484
|
+
if (error) {
|
|
485
|
+
console.error('[RBACEngine] Failed to load role context:', error);
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
513
488
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
*
|
|
517
|
-
* @param scope - Permission scope
|
|
518
|
-
* @param appId - Optional app ID
|
|
519
|
-
* @returns Promise resolving to validated scope with resolved organisation ID
|
|
520
|
-
*/
|
|
521
|
-
private async validateContextRequirements(scope: Scope, appId?: UUID): Promise<Scope | null> {
|
|
522
|
-
// If we have an app ID, check its requirements
|
|
523
|
-
if (appId) {
|
|
524
|
-
const appConfig = await this.getAppConfig(appId);
|
|
525
|
-
if (!appConfig) {
|
|
526
|
-
return null; // App not found or inactive
|
|
489
|
+
if (!Array.isArray(data)) {
|
|
490
|
+
return result;
|
|
527
491
|
}
|
|
528
492
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
return null; // Event context required
|
|
493
|
+
for (const permission of data as RBACPermission[]) {
|
|
494
|
+
if (permission.permission_type === 'all_permissions') {
|
|
495
|
+
result.globalRole = 'super_admin';
|
|
533
496
|
}
|
|
534
497
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const resolvedOrgId = await this.resolveOrganisationFromEvent(scope.eventId);
|
|
538
|
-
if (!resolvedOrgId) {
|
|
539
|
-
return null; // Could not resolve organisation from event
|
|
540
|
-
}
|
|
541
|
-
return {
|
|
542
|
-
...scope,
|
|
543
|
-
organisationId: resolvedOrgId
|
|
544
|
-
};
|
|
498
|
+
if (permission.permission_type === 'organisation_access') {
|
|
499
|
+
result.organisationRole = permission.role_name as any;
|
|
545
500
|
}
|
|
546
501
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// Organisation-based app: requires organisationId, eventId is optional
|
|
550
|
-
if (!scope.organisationId) {
|
|
551
|
-
return null; // Organisation context required
|
|
502
|
+
if (permission.permission_type === 'event_app_access') {
|
|
503
|
+
result.eventAppRole = permission.role_name as any;
|
|
552
504
|
}
|
|
553
|
-
|
|
554
|
-
return scope;
|
|
555
505
|
}
|
|
556
|
-
}
|
|
557
506
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
507
|
+
return result;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error('[RBACEngine] Unexpected error loading role context:', error);
|
|
510
|
+
return result;
|
|
561
511
|
}
|
|
562
|
-
|
|
563
|
-
return scope;
|
|
564
512
|
}
|
|
565
513
|
|
|
566
514
|
/**
|
|
567
|
-
*
|
|
515
|
+
* Check if user is super admin
|
|
568
516
|
*
|
|
569
517
|
* @param userId - User ID
|
|
570
|
-
* @
|
|
571
|
-
* @param pageId - Optional page ID
|
|
572
|
-
* @returns Promise resolving to grants array
|
|
573
|
-
*
|
|
574
|
-
* PRECEDENCE ORDER (closest scope first): page → eventApp → organisation → global
|
|
518
|
+
* @returns Promise resolving to super admin status
|
|
575
519
|
*/
|
|
576
|
-
private async
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const now = new Date().toISOString();
|
|
583
|
-
|
|
584
|
-
// Pre-fetch user roles once (to avoid duplicate queries)
|
|
585
|
-
const userRoles: string[] = [];
|
|
586
|
-
|
|
587
|
-
// Get event-app roles
|
|
588
|
-
if (scope.eventId && scope.appId) {
|
|
589
|
-
const { data: eventRoles } = await this.supabase
|
|
590
|
-
.from('rbac_event_app_roles')
|
|
591
|
-
.select('role, status, valid_from, valid_to')
|
|
592
|
-
.eq('user_id', userId)
|
|
593
|
-
.eq('event_id', scope.eventId)
|
|
594
|
-
.eq('app_id', scope.appId)
|
|
595
|
-
.eq('status', 'active')
|
|
596
|
-
.lte('valid_from', now)
|
|
597
|
-
.or(`valid_to.is.null,valid_to.gte.${now}`) as { data: Array<{ role: string; status: string; valid_from: string; valid_to: string | null }> | null };
|
|
598
|
-
|
|
599
|
-
if (eventRoles) {
|
|
600
|
-
userRoles.push(...eventRoles.map(r => r.role));
|
|
601
|
-
// Store event-app roles for later permission lookup
|
|
602
|
-
// The actual permissions will be looked up from rbac_page_permissions table
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// Get organisation roles
|
|
607
|
-
if (scope.organisationId) {
|
|
608
|
-
const { data: orgRoles } = await this.supabase
|
|
609
|
-
.from('rbac_organisation_roles')
|
|
610
|
-
.select('role, status, valid_from, valid_to')
|
|
611
|
-
.eq('user_id', userId)
|
|
612
|
-
.eq('organisation_id', scope.organisationId)
|
|
613
|
-
.eq('status', 'active')
|
|
614
|
-
.lte('valid_from', now)
|
|
615
|
-
.or(`valid_to.is.null,valid_to.gte.${now}`) as { data: Array<{ role: string; status: string; valid_from: string; valid_to: string | null }> | null };
|
|
616
|
-
|
|
617
|
-
if (orgRoles) {
|
|
618
|
-
userRoles.push(...orgRoles.map(r => r.role));
|
|
619
|
-
// Store organisation roles for later permission lookup
|
|
620
|
-
// The actual permissions will be looked up from rbac_page_permissions table
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
console.log('[collectActiveGrants] User roles:', userRoles);
|
|
625
|
-
|
|
626
|
-
// 1. PAGE GRANTS (closest scope first) - Use RPC to bypass RLS
|
|
627
|
-
if (pageId) {
|
|
628
|
-
// Resolve page ID if it's a page name (string)
|
|
629
|
-
let resolvedPageId: UUID | null = null;
|
|
630
|
-
if (typeof pageId === 'string') {
|
|
631
|
-
// Check if it's already a UUID
|
|
632
|
-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
633
|
-
if (uuidRegex.test(pageId)) {
|
|
634
|
-
resolvedPageId = pageId as UUID;
|
|
635
|
-
} else {
|
|
636
|
-
// It's a page name, resolve it to a page ID
|
|
637
|
-
const appId = scope.appId;
|
|
638
|
-
if (appId) {
|
|
639
|
-
const { data: page } = await this.supabase
|
|
640
|
-
.from('rbac_app_pages')
|
|
641
|
-
.select('id')
|
|
642
|
-
.eq('app_id', appId)
|
|
643
|
-
.eq('page_name', pageId)
|
|
644
|
-
.single() as { data: { id: UUID } | null };
|
|
645
|
-
resolvedPageId = page?.id || null;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
} else {
|
|
649
|
-
resolvedPageId = pageId;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (resolvedPageId && scope.appId) {
|
|
653
|
-
console.log('[collectActiveGrants] Fetching page permissions via RPC for page:', resolvedPageId);
|
|
654
|
-
|
|
655
|
-
// Get the page name for building specific permission strings
|
|
656
|
-
let pageName: string | null = null;
|
|
657
|
-
if (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)) {
|
|
658
|
-
// pageId is already the page name
|
|
659
|
-
pageName = pageId;
|
|
660
|
-
} else {
|
|
661
|
-
// Fetch page name from database
|
|
662
|
-
const { data: page } = await this.supabase
|
|
663
|
-
.from('rbac_app_pages')
|
|
664
|
-
.select('page_name')
|
|
665
|
-
.eq('id', resolvedPageId)
|
|
666
|
-
.single() as { data: { page_name: string } | null };
|
|
667
|
-
pageName = page?.page_name || null;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Use RPC to get page permissions (bypasses RLS)
|
|
671
|
-
// @ts-ignore - RPC type inference is incorrect
|
|
672
|
-
const rpcResult = await this.supabase.rpc('rbac_permissions_get', {
|
|
673
|
-
p_user_id: userId,
|
|
674
|
-
p_app_id: scope.appId,
|
|
675
|
-
p_event_id: scope.eventId || null,
|
|
676
|
-
p_organisation_id: scope.organisationId || null,
|
|
677
|
-
p_page_id: resolvedPageId
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
const { data: rpcPermissions } = rpcResult as { data: Array<{ permission_type: string; role_name: string; has_permission: boolean }> | null; error: any };
|
|
681
|
-
|
|
682
|
-
console.log('[collectActiveGrants] RPC page permissions:', rpcPermissions);
|
|
683
|
-
|
|
684
|
-
if (rpcPermissions && pageName) {
|
|
685
|
-
// Filter to only page-level permissions (not org/event-app roles)
|
|
686
|
-
const pagePerms = rpcPermissions.filter(p =>
|
|
687
|
-
p.permission_type !== 'all_permissions' &&
|
|
688
|
-
p.permission_type !== 'organisation_access' &&
|
|
689
|
-
p.permission_type !== 'event_app_access'
|
|
690
|
-
);
|
|
691
|
-
|
|
692
|
-
for (const perm of pagePerms) {
|
|
693
|
-
// Only add if user has this role
|
|
694
|
-
if (userRoles.includes(perm.role_name)) {
|
|
695
|
-
console.log('[collectActiveGrants] Adding page grant:', { operation: perm.permission_type, role: perm.role_name, allowed: perm.has_permission, pageName });
|
|
696
|
-
grants.push({
|
|
697
|
-
type: perm.has_permission ? 'allow' : 'deny',
|
|
698
|
-
// Use permission_type directly as it already includes the page name
|
|
699
|
-
permission: perm.permission_type as Permission,
|
|
700
|
-
scope: 'page',
|
|
701
|
-
source: 'rbac_page_permissions',
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
520
|
+
private async checkSuperAdmin(userId: UUID): Promise<boolean> {
|
|
521
|
+
// Check cache first
|
|
522
|
+
const cacheKey = `super_admin:${userId}`;
|
|
523
|
+
const cached = rbacCache.get<boolean>(cacheKey);
|
|
524
|
+
if (cached !== null) {
|
|
525
|
+
return cached;
|
|
707
526
|
}
|
|
708
527
|
|
|
709
|
-
|
|
710
|
-
const { data
|
|
528
|
+
const now = new Date().toISOString();
|
|
529
|
+
const { data, error } = await this.supabase
|
|
711
530
|
.from('rbac_global_roles')
|
|
712
|
-
.select('role
|
|
531
|
+
.select('role')
|
|
713
532
|
.eq('user_id', userId)
|
|
533
|
+
.eq('role', 'super_admin')
|
|
714
534
|
.lte('valid_from', now)
|
|
715
|
-
.or(`valid_to.is.null,valid_to.gte.${now}`)
|
|
535
|
+
.or(`valid_to.is.null,valid_to.gte.${now}`)
|
|
536
|
+
.limit(1) as { data: Array<{ role: string }> | null; error: any };
|
|
716
537
|
|
|
717
|
-
|
|
718
|
-
for (const role of globalRoles) {
|
|
719
|
-
if (role.role === 'super_admin') {
|
|
720
|
-
// Super admin gets all CRUD permissions
|
|
721
|
-
grants.push(
|
|
722
|
-
{ type: 'allow', permission: 'read:*' as Permission, scope: 'global', source: 'rbac_global_roles' },
|
|
723
|
-
{ type: 'allow', permission: 'create:*' as Permission, scope: 'global', source: 'rbac_global_roles' },
|
|
724
|
-
{ type: 'allow', permission: 'update:*' as Permission, scope: 'global', source: 'rbac_global_roles' },
|
|
725
|
-
{ type: 'allow', permission: 'delete:*' as Permission, scope: 'global', source: 'rbac_global_roles' }
|
|
726
|
-
);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
538
|
+
const isSuperAdmin = !error && data && data.length > 0;
|
|
730
539
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
return grants;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Check page-specific permissions
|
|
738
|
-
*
|
|
739
|
-
* @param userId - User ID
|
|
740
|
-
* @param pageId - Page ID
|
|
741
|
-
* @param permission - Permission to check
|
|
742
|
-
* @param scope - Permission scope
|
|
743
|
-
* @returns Promise resolving to page permission result
|
|
744
|
-
*/
|
|
745
|
-
private async checkPagePermissions(
|
|
746
|
-
userId: UUID,
|
|
747
|
-
pageId: UUID | string | undefined,
|
|
748
|
-
permission: Permission,
|
|
749
|
-
scope: Scope
|
|
750
|
-
): Promise<boolean> {
|
|
751
|
-
if (!pageId) {
|
|
752
|
-
return true; // No page restrictions
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const [operation] = permission.split(':') as [Operation, string];
|
|
756
|
-
|
|
757
|
-
// Get user's roles in this scope
|
|
758
|
-
const userRoles: string[] = [];
|
|
759
|
-
|
|
760
|
-
// Add organisation role
|
|
761
|
-
if (scope.organisationId) {
|
|
762
|
-
const orgRole = await this.getOrganisationRole(userId, scope.organisationId);
|
|
763
|
-
if (orgRole) {
|
|
764
|
-
userRoles.push(orgRole);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Add event-app role
|
|
769
|
-
if (scope.eventId && scope.appId) {
|
|
770
|
-
const eventRole = await this.getEventAppRole(userId, scope.eventId, scope.appId);
|
|
771
|
-
if (eventRole) {
|
|
772
|
-
userRoles.push(eventRole);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Resolve page ID if it's a page name (string)
|
|
777
|
-
let resolvedPageId: UUID | null = null;
|
|
778
|
-
if (typeof pageId === 'string') {
|
|
779
|
-
// Check if it's already a UUID
|
|
780
|
-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
781
|
-
if (uuidRegex.test(pageId)) {
|
|
782
|
-
resolvedPageId = pageId as UUID;
|
|
783
|
-
} else {
|
|
784
|
-
// It's a page name, resolve it to a page ID
|
|
785
|
-
// First, get the app ID from scope or environment
|
|
786
|
-
let appId = scope.appId;
|
|
787
|
-
if (!appId) {
|
|
788
|
-
// Try to get app ID from environment
|
|
789
|
-
const appName = import.meta.env.VITE_APP_NAME || import.meta.env.REACT_APP_NAME;
|
|
790
|
-
if (appName) {
|
|
791
|
-
const { data: app } = await this.supabase
|
|
792
|
-
.from('rbac_apps')
|
|
793
|
-
.select('id')
|
|
794
|
-
.eq('name', appName)
|
|
795
|
-
.eq('is_active', true)
|
|
796
|
-
.single() as { data: { id: UUID } | null };
|
|
797
|
-
if (app) {
|
|
798
|
-
appId = app.id;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (appId) {
|
|
804
|
-
const { data: page } = await this.supabase
|
|
805
|
-
.from('rbac_app_pages')
|
|
806
|
-
.select('id')
|
|
807
|
-
.eq('app_id', appId)
|
|
808
|
-
.eq('page_name', pageId)
|
|
809
|
-
.single() as { data: { id: UUID } | null };
|
|
810
|
-
resolvedPageId = page?.id || null;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
} else {
|
|
814
|
-
resolvedPageId = pageId;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if (!resolvedPageId) {
|
|
818
|
-
return false; // Page not found
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Check page permissions
|
|
822
|
-
const { data: pagePermissions } = await this.supabase
|
|
823
|
-
.from('rbac_page_permissions')
|
|
824
|
-
.select('allowed')
|
|
825
|
-
.eq('app_page_id', resolvedPageId)
|
|
826
|
-
.eq('operation', operation)
|
|
827
|
-
.in('role_name', userRoles)
|
|
828
|
-
.single() as { data: { allowed: boolean } | null };
|
|
829
|
-
|
|
830
|
-
return pagePermissions?.allowed ?? false;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Get organisation role for a user
|
|
835
|
-
*
|
|
836
|
-
* @param userId - User ID
|
|
837
|
-
* @param organisationId - Organisation ID
|
|
838
|
-
* @returns Promise resolving to organisation role
|
|
839
|
-
*/
|
|
840
|
-
private async getOrganisationRole(userId: UUID, organisationId: UUID): Promise<OrganisationRole | null> {
|
|
841
|
-
const { data, error } = await this.supabase
|
|
842
|
-
.from('rbac_organisation_roles')
|
|
843
|
-
.select('role')
|
|
844
|
-
.eq('user_id', userId)
|
|
845
|
-
.eq('organisation_id', organisationId)
|
|
846
|
-
.eq('status', 'active')
|
|
847
|
-
.single() as { data: { role: string } | null; error: any };
|
|
848
|
-
|
|
849
|
-
return error ? null : (data?.role as OrganisationRole) || null;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* Get event-app role for a user
|
|
854
|
-
*
|
|
855
|
-
* @param userId - User ID
|
|
856
|
-
* @param eventId - Event ID
|
|
857
|
-
* @param appId - App ID
|
|
858
|
-
* @returns Promise resolving to event-app role
|
|
859
|
-
*/
|
|
860
|
-
private async getEventAppRole(userId: UUID, eventId: string, appId: UUID): Promise<EventAppRole | null> {
|
|
861
|
-
const { data, error } = await this.supabase
|
|
862
|
-
.from('rbac_event_app_roles')
|
|
863
|
-
.select('role, status, valid_from, valid_to')
|
|
864
|
-
.eq('user_id', userId)
|
|
865
|
-
.eq('event_id', eventId)
|
|
866
|
-
.eq('app_id', appId)
|
|
867
|
-
.eq('status', 'active')
|
|
868
|
-
.lte('valid_from', new Date().toISOString())
|
|
869
|
-
.or(`valid_to.is.null,valid_to.gte.${new Date().toISOString()}`)
|
|
870
|
-
.single() as { data: { role: string; status: string; valid_from: string; valid_to: string | null } | null; error: any };
|
|
871
|
-
|
|
872
|
-
return error ? null : (data?.role as EventAppRole) || null;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
/**
|
|
876
|
-
* Get permission for organisation role
|
|
877
|
-
*
|
|
878
|
-
* @param role - Organisation role
|
|
879
|
-
* @returns Permission string
|
|
880
|
-
*/
|
|
881
|
-
private getPermissionForOrgRole(role: OrganisationRole): Permission {
|
|
882
|
-
switch (role) {
|
|
883
|
-
case 'org_admin':
|
|
884
|
-
return 'read:*' as Permission; // Will be expanded to all CRUD in collectActiveGrants
|
|
885
|
-
case 'leader':
|
|
886
|
-
return 'read:organisation.*' as Permission; // Will be expanded to all CRUD in collectActiveGrants
|
|
887
|
-
case 'member':
|
|
888
|
-
return 'read:organisation.*' as Permission;
|
|
889
|
-
case 'supporter':
|
|
890
|
-
return 'read:organisation.public' as Permission;
|
|
891
|
-
default:
|
|
892
|
-
return 'read:organisation.public' as Permission;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Get permission for event-app role
|
|
898
|
-
*
|
|
899
|
-
* @param role - Event-app role
|
|
900
|
-
* @returns Permission string
|
|
901
|
-
*/
|
|
902
|
-
private getPermissionForEventRole(role: EventAppRole): Permission {
|
|
903
|
-
switch (role) {
|
|
904
|
-
case 'event_admin':
|
|
905
|
-
return 'read:event.*' as Permission; // Will be expanded to all CRUD in collectActiveGrants
|
|
906
|
-
case 'planner':
|
|
907
|
-
return 'read:event.planning' as Permission; // Will be expanded to all CRUD in collectActiveGrants
|
|
908
|
-
case 'participant':
|
|
909
|
-
return 'read:event.*' as Permission;
|
|
910
|
-
case 'viewer':
|
|
911
|
-
return 'read:event.public' as Permission;
|
|
912
|
-
default:
|
|
913
|
-
return 'read:event.public' as Permission;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/**
|
|
918
|
-
* Check if a permission matches another permission
|
|
919
|
-
*
|
|
920
|
-
* @param grantPermission - Permission from grant
|
|
921
|
-
* @param requestedPermission - Requested permission
|
|
922
|
-
* @returns True if permissions match
|
|
923
|
-
*/
|
|
924
|
-
private permissionMatches(grantPermission: Permission, requestedPermission: Permission): boolean {
|
|
925
|
-
console.log('[permissionMatches] Checking:', { grantPermission, requestedPermission });
|
|
540
|
+
// Cache for 60 seconds
|
|
541
|
+
rbacCache.set(cacheKey, isSuperAdmin, 60000);
|
|
926
542
|
|
|
927
|
-
|
|
928
|
-
if (grantPermission === requestedPermission) {
|
|
929
|
-
console.log('[permissionMatches] Exact match found');
|
|
930
|
-
return true;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// Wildcard match
|
|
934
|
-
if (grantPermission.endsWith(':*') || grantPermission.endsWith('.*')) {
|
|
935
|
-
const [grantOp, grantResource] = grantPermission.split(':');
|
|
936
|
-
const [requestedOp, requestedResource] = requestedPermission.split(':');
|
|
937
|
-
|
|
938
|
-
console.log('[permissionMatches] Wildcard check:', {
|
|
939
|
-
grantOp,
|
|
940
|
-
grantResource,
|
|
941
|
-
requestedOp,
|
|
942
|
-
requestedResource,
|
|
943
|
-
operationsMatch: grantOp === requestedOp
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
if (grantOp === requestedOp) {
|
|
947
|
-
// For wildcard permissions like "read:*" or "read:organisation.*", grantResource is "*" or "organisation.*"
|
|
948
|
-
// We need to check if the requested resource starts with the prefix before "*"
|
|
949
|
-
const prefix = grantResource.slice(0, -1); // Remove the "*"
|
|
950
|
-
const matches = prefix === '' || requestedResource.startsWith(prefix);
|
|
951
|
-
console.log('[permissionMatches] Wildcard match result:', { prefix, matches });
|
|
952
|
-
return matches;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// Check for other wildcard patterns
|
|
957
|
-
if (grantPermission.includes('*')) {
|
|
958
|
-
const [grantOp, grantResource] = grantPermission.split(':');
|
|
959
|
-
const [requestedOp, requestedResource] = requestedPermission.split(':');
|
|
960
|
-
|
|
961
|
-
console.log('[permissionMatches] Other wildcard check:', {
|
|
962
|
-
grantOp,
|
|
963
|
-
grantResource,
|
|
964
|
-
requestedOp,
|
|
965
|
-
requestedResource,
|
|
966
|
-
operationsMatch: grantOp === requestedOp
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
if (grantOp === requestedOp) {
|
|
970
|
-
// For wildcard permissions like "read:event*", grantResource is "event*"
|
|
971
|
-
const prefix = grantResource.replace('*', '');
|
|
972
|
-
const matches = requestedResource.startsWith(prefix);
|
|
973
|
-
console.log('[permissionMatches] Other wildcard match result:', { prefix, matches });
|
|
974
|
-
return matches;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
console.log('[permissionMatches] No match found');
|
|
979
|
-
return false;
|
|
543
|
+
return Boolean(isSuperAdmin);
|
|
980
544
|
}
|
|
981
545
|
|
|
982
546
|
/**
|
|
@@ -984,7 +548,7 @@ export class RBACEngine {
|
|
|
984
548
|
*
|
|
985
549
|
* @param pageId - Page ID (UUID) or page name (string)
|
|
986
550
|
* @param appId - App ID to look up the page
|
|
987
|
-
* @returns Resolved page ID (UUID) or original pageId
|
|
551
|
+
* @returns Resolved page ID (UUID) or original pageId
|
|
988
552
|
*/
|
|
989
553
|
private async resolvePageId(pageId?: UUID | string, appId?: UUID): Promise<UUID | string | undefined> {
|
|
990
554
|
if (!pageId) {
|
|
@@ -999,11 +563,10 @@ export class RBACEngine {
|
|
|
999
563
|
|
|
1000
564
|
// It's a page name, but we need appId to resolve it
|
|
1001
565
|
if (!appId) {
|
|
1002
|
-
// If we can't resolve it, return the original value
|
|
1003
566
|
return pageId;
|
|
1004
567
|
}
|
|
1005
568
|
|
|
1006
|
-
//
|
|
569
|
+
// Resolve page name to UUID
|
|
1007
570
|
try {
|
|
1008
571
|
const { data: page } = await this.supabase
|
|
1009
572
|
.from('rbac_app_pages')
|
|
@@ -1012,10 +575,10 @@ export class RBACEngine {
|
|
|
1012
575
|
.eq('page_name', pageId)
|
|
1013
576
|
.single() as { data: { id: UUID } | null };
|
|
1014
577
|
|
|
1015
|
-
return page?.id || pageId;
|
|
578
|
+
return page?.id || pageId;
|
|
1016
579
|
} catch (error) {
|
|
1017
580
|
console.warn('[RBAC Engine] Failed to resolve page name to UUID:', { pageId, appId, error });
|
|
1018
|
-
return pageId;
|
|
581
|
+
return pageId;
|
|
1019
582
|
}
|
|
1020
583
|
}
|
|
1021
584
|
}
|
|
@@ -1029,3 +592,4 @@ export class RBACEngine {
|
|
|
1029
592
|
export function createRBACEngine(supabase: SupabaseClient<Database>): RBACEngine {
|
|
1030
593
|
return new RBACEngine(supabase);
|
|
1031
594
|
}
|
|
595
|
+
|