@jmruthers/pace-core 0.5.191 → 0.6.1
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 +29 -0
- package/README.md +7 -1
- package/cursor-rules/00-pace-core-compliance.mdc +372 -0
- package/cursor-rules/01-standards-compliance.mdc +275 -0
- package/cursor-rules/02-project-structure.mdc +200 -0
- package/cursor-rules/03-solid-principles.mdc +341 -0
- package/cursor-rules/04-testing-standards.mdc +315 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +246 -0
- package/cursor-rules/06-code-quality.mdc +392 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +309 -0
- package/cursor-rules/CHANGELOG.md +101 -0
- package/cursor-rules/README.md +191 -0
- package/dist/{AuthService-CbP_utw2.d.ts → AuthService-DjnJHDtC.d.ts} +1 -0
- package/dist/{DataTable-Be6dH_dR.d.ts → DataTable-CH1U5Tpy.d.ts} +1 -1
- package/dist/{DataTable-WKRZD47S.js → DataTable-DQ7RSOHE.js} +8 -7
- package/dist/{PublicPageProvider-ULXC_u6U.d.ts → PublicPageProvider-ce4xlHYA.d.ts} +37 -156
- package/dist/{UnifiedAuthProvider-BYA9qB-o.d.ts → UnifiedAuthProvider-185Ih4dj.d.ts} +2 -0
- package/dist/{UnifiedAuthProvider-FTSG5XH7.js → UnifiedAuthProvider-ATAP5UTR.js} +3 -3
- package/dist/{api-IHKALJZD.js → api-N774RPUA.js} +2 -2
- package/dist/{chunk-6C4YBBJM.js → chunk-3QRJFVBR.js} +1 -1
- package/dist/chunk-3QRJFVBR.js.map +1 -0
- package/dist/{chunk-OETXORNB.js → chunk-3XTALGJF.js} +211 -136
- package/dist/chunk-3XTALGJF.js.map +1 -0
- package/dist/{chunk-6TQDD426.js → chunk-4N5C5XZU.js} +47 -228
- package/dist/chunk-4N5C5XZU.js.map +1 -0
- package/dist/{chunk-LOMZXPSN.js → chunk-4ZC4GX36.js} +47 -74
- package/dist/chunk-4ZC4GX36.js.map +1 -0
- package/dist/{chunk-6LTQQAT6.js → chunk-BYFSK72L.js} +357 -158
- package/dist/chunk-BYFSK72L.js.map +1 -0
- package/dist/{chunk-XYXSXPUK.js → chunk-EXUD6RNJ.js} +50 -10
- package/dist/chunk-EXUD6RNJ.js.map +1 -0
- package/dist/{chunk-VKB2CO4Z.js → chunk-GLK6VM3F.js} +244 -249
- package/dist/chunk-GLK6VM3F.js.map +1 -0
- package/dist/{chunk-HW3OVDUF.js → chunk-J36DSWQK.js} +1 -1
- package/dist/{chunk-HW3OVDUF.js.map → chunk-J36DSWQK.js.map} +1 -1
- package/dist/{chunk-XNYQOL3Z.js → chunk-JBKQ3SAO.js} +9 -18
- package/dist/chunk-JBKQ3SAO.js.map +1 -0
- package/dist/{chunk-ROXMHMY2.js → chunk-KNC55RTG.js} +13 -3
- package/dist/{chunk-ROXMHMY2.js.map → chunk-KNC55RTG.js.map} +1 -1
- package/dist/{chunk-QWWZ5CAQ.js → chunk-LXQLPRQ2.js} +2 -2
- package/dist/{chunk-ULHIJK66.js → chunk-T33XF5ZC.js} +255 -140
- package/dist/chunk-T33XF5ZC.js.map +1 -0
- package/dist/{chunk-VRGWKHDB.js → chunk-XM25TVIE.js} +100 -33
- package/dist/chunk-XM25TVIE.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +9 -9
- package/dist/hooks.d.ts +6 -6
- package/dist/hooks.js +20 -25
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +18 -21
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +2 -20
- package/dist/rbac/index.js +7 -9
- package/dist/{usePublicRouteParams-TZe0gy-4.d.ts → usePublicRouteParams-BJAlWfuJ.d.ts} +3 -3
- package/dist/{useToast-C8gR5ir4.d.ts → useToast-AyaT-x7p.d.ts} +2 -2
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +3 -3
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +2 -2
- package/docs/api/classes/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +10 -10
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.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/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +24 -11
- 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/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +2 -2
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- 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/ParsedAddress.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +4 -4
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.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/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +2 -2
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +2 -2
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +2 -2
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +2 -2
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +2 -2
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +2 -2
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +2 -2
- package/docs/api/interfaces/RouteConfig.md +2 -2
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.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 +60 -38
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- 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/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +194 -209
- package/docs/getting-started/cursor-rules.md +262 -0
- package/docs/getting-started/installation-guide.md +6 -1
- package/docs/getting-started/quick-start.md +6 -1
- package/docs/migration/MIGRATION_GUIDE.md +4 -4
- package/docs/migration/REACT_19_MIGRATION.md +227 -0
- package/docs/migration/database-changes-december-2025.md +2 -1
- package/docs/rbac/event-based-apps.md +124 -6
- package/docs/standards/README.md +39 -0
- package/docs/troubleshooting/migration.md +4 -4
- package/examples/PublicPages/PublicEventPage.tsx +1 -1
- package/package.json +11 -6
- package/scripts/audit-consuming-app.cjs +961 -0
- package/scripts/check-pace-core-compliance.cjs +315 -61
- package/scripts/install-cursor-rules.cjs +236 -0
- package/src/__tests__/helpers/test-providers.tsx +1 -1
- package/src/__tests__/helpers/test-utils.tsx +1 -1
- package/src/__tests__/rls-policies.test.ts +3 -1
- package/src/components/Badge/Badge.tsx +2 -4
- package/src/components/Button/Button.tsx +5 -4
- package/src/components/Calendar/Calendar.tsx +1 -1
- package/src/components/DataTable/DataTable.test.tsx +57 -93
- package/src/components/DataTable/DataTable.tsx +2 -2
- package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +172 -45
- package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +121 -28
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +9 -8
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +20 -52
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +170 -34
- package/src/components/DataTable/__tests__/keyboard.test.tsx +75 -12
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +88 -16
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +12 -12
- package/src/components/DataTable/components/AccessDeniedPage.tsx +1 -1
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
- package/src/components/DataTable/components/DataTableCore.tsx +4 -7
- package/src/components/DataTable/components/DataTableModals.tsx +1 -1
- package/src/components/DataTable/components/EditableRow.tsx +1 -1
- package/src/components/DataTable/components/UnifiedTableBody.tsx +86 -17
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +23 -23
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +11 -11
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +36 -36
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +27 -27
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +39 -39
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +33 -33
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +29 -29
- package/src/components/DataTable/hooks/useColumnReordering.ts +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +75 -10
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +2 -2
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -14
- package/src/components/Dialog/Dialog.tsx +6 -5
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +1 -1
- package/src/components/EventSelector/EventSelector.tsx +1 -1
- package/src/components/FileDisplay/FileDisplay.test.tsx +4 -3
- package/src/components/FileDisplay/FileDisplay.tsx +16 -4
- package/src/components/Footer/Footer.tsx +1 -1
- package/src/components/Form/Form.test.tsx +36 -15
- package/src/components/Form/Form.tsx +30 -26
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +40 -40
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +1 -1
- package/src/components/Input/Input.tsx +28 -30
- package/src/components/Label/Label.tsx +1 -1
- package/src/components/LoadingSpinner/LoadingSpinner.tsx +1 -1
- package/src/components/LoginForm/LoginForm.test.tsx +42 -42
- package/src/components/LoginForm/LoginForm.tsx +8 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +6 -4
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -11
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -1
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +1 -1
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +75 -52
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +98 -69
- package/src/components/PaceAppLayout/README.md +1 -1
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -8
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +33 -33
- package/src/components/PasswordChange/PasswordChangeForm.tsx +1 -1
- package/src/components/Progress/Progress.tsx +1 -1
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +5 -9
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +0 -1
- package/src/components/PublicLayout/PublicPageLayout.tsx +1 -1
- package/src/components/PublicLayout/PublicPageProvider.tsx +0 -1
- package/src/components/Select/Select.tsx +33 -22
- package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +1 -1
- package/src/components/Table/Table.tsx +1 -1
- package/src/components/Textarea/Textarea.tsx +27 -29
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/components/Tooltip/Tooltip.tsx +1 -1
- package/src/components/UserMenu/UserMenu.tsx +1 -1
- package/src/hooks/__tests__/hooks.integration.test.tsx +80 -55
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +14 -7
- package/src/hooks/__tests__/useStorage.unit.test.ts +36 -36
- package/src/hooks/public/usePublicEvent.ts +1 -1
- package/src/hooks/public/usePublicEventLogo.ts +1 -1
- package/src/hooks/public/usePublicRouteParams.ts +1 -1
- package/src/hooks/services/useAuthService.ts +21 -3
- package/src/hooks/services/useEventService.ts +21 -3
- package/src/hooks/services/useInactivityService.ts +21 -3
- package/src/hooks/services/useOrganisationService.ts +21 -3
- package/src/hooks/useDataTableState.ts +8 -18
- package/src/hooks/useFileDisplay.ts +10 -17
- package/src/hooks/useFocusManagement.ts +2 -2
- package/src/hooks/useFocusTrap.ts +4 -4
- package/src/hooks/useFormDialog.ts +8 -7
- package/src/hooks/useInactivityTracker.ts +1 -1
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/hooks/useSecureDataAccess.test.ts +16 -9
- package/src/hooks/useSecureDataAccess.ts +22 -6
- package/src/hooks/useToast.ts +2 -2
- package/src/providers/__tests__/OrganisationProvider.test.tsx +57 -13
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +21 -6
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +10 -10
- package/src/providers/services/EventServiceProvider.tsx +0 -8
- package/src/providers/services/UnifiedAuthProvider.tsx +196 -46
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +13 -3
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +34 -40
- package/src/rbac/__tests__/isSuperAdmin.real.test.ts +82 -0
- package/src/rbac/adapters.tsx +3 -22
- package/src/rbac/api.test.ts +2 -2
- package/src/rbac/api.ts +7 -1
- package/src/rbac/components/EnhancedNavigationMenu.tsx +3 -16
- package/src/rbac/components/NavigationGuard.tsx +2 -11
- package/src/rbac/components/NavigationProvider.tsx +1 -2
- package/src/rbac/components/PagePermissionGuard.tsx +1 -1
- package/src/rbac/components/PagePermissionProvider.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +46 -13
- package/src/rbac/components/RoleBasedRouter.tsx +1 -1
- package/src/rbac/components/SecureDataProvider.tsx +1 -2
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +7 -43
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +4 -11
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +3 -3
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +1 -1
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +1 -1
- package/src/rbac/engine.ts +14 -2
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/usePermissions.ts +51 -11
- package/src/rbac/hooks/useRBAC.ts +3 -13
- package/src/rbac/hooks/useResolvedScope.test.ts +75 -54
- package/src/rbac/hooks/useResolvedScope.ts +58 -33
- package/src/rbac/hooks/useSecureSupabase.ts +4 -9
- package/src/rbac/secureClient.ts +43 -0
- package/src/services/EventService.ts +4 -57
- package/src/services/InactivityService.ts +127 -34
- package/src/services/OrganisationService.ts +68 -10
- package/src/utils/security/secureDataAccess.test.ts +31 -20
- package/src/utils/security/secureDataAccess.ts +4 -3
- package/dist/chunk-6C4YBBJM.js.map +0 -1
- package/dist/chunk-6LTQQAT6.js.map +0 -1
- package/dist/chunk-6TQDD426.js.map +0 -1
- package/dist/chunk-LOMZXPSN.js.map +0 -1
- package/dist/chunk-OETXORNB.js.map +0 -1
- package/dist/chunk-ULHIJK66.js.map +0 -1
- package/dist/chunk-VKB2CO4Z.js.map +0 -1
- package/dist/chunk-VRGWKHDB.js.map +0 -1
- package/dist/chunk-XNYQOL3Z.js.map +0 -1
- package/dist/chunk-XYXSXPUK.js.map +0 -1
- package/scripts/check-pace-core-compliance.js +0 -512
- package/src/rbac/hooks/useSuperAdminBypass.ts +0 -126
- package/src/utils/context/superAdminOverride.ts +0 -58
- /package/dist/{DataTable-WKRZD47S.js.map → DataTable-DQ7RSOHE.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-FTSG5XH7.js.map → UnifiedAuthProvider-ATAP5UTR.js.map} +0 -0
- /package/dist/{api-IHKALJZD.js.map → api-N774RPUA.js.map} +0 -0
- /package/dist/{chunk-QWWZ5CAQ.js.map → chunk-LXQLPRQ2.js.map} +0 -0
- /package/examples/{rbac → RBAC}/CompleteRBACExample.tsx +0 -0
- /package/examples/{rbac → RBAC}/EventBasedApp.tsx +0 -0
- /package/examples/{rbac → RBAC}/PermissionExample.tsx +0 -0
- /package/examples/{rbac → RBAC}/index.ts +0 -0
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
* - Permission-based route protection
|
|
87
87
|
*
|
|
88
88
|
* @dependencies
|
|
89
|
-
* - React
|
|
89
|
+
* - React 19+ - Component framework
|
|
90
90
|
* - React Router v6 - Routing
|
|
91
91
|
* - UnifiedAuthProvider - Authentication
|
|
92
92
|
* - usePermissionCache - Permission management
|
|
@@ -103,10 +103,10 @@ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
|
103
103
|
import { useOrganisations } from '../../hooks/useOrganisations';
|
|
104
104
|
import { useEvents } from '../../hooks/useEvents';
|
|
105
105
|
import { useEventTheme } from '../../hooks/useEventTheme';
|
|
106
|
-
import { useCan, useResolvedScope } from '../../rbac/hooks';
|
|
106
|
+
import { useCan, useResolvedScope, useRBAC } from '../../rbac/hooks';
|
|
107
107
|
import { createScopeFromEvent } from '../../rbac/utils/eventContext';
|
|
108
108
|
import { getCurrentAppName } from '../../utils/app/appNameResolver';
|
|
109
|
-
import { isSuperAdmin } from '../../rbac/api';
|
|
109
|
+
import { isSuperAdmin as checkSuperAdminApi } from '../../rbac/api';
|
|
110
110
|
import { logger } from '../../utils/core/logger';
|
|
111
111
|
import type { Permission, Scope } from '../../rbac/types';
|
|
112
112
|
|
|
@@ -372,7 +372,7 @@ export function PaceAppLayout({
|
|
|
372
372
|
onRouteAccessDenied,
|
|
373
373
|
onRouteStrictModeViolation
|
|
374
374
|
}: PaceAppLayoutProps) {
|
|
375
|
-
const { user, signOut, updatePassword, supabase, appId: contextAppId } = useUnifiedAuth(); // Get appId from context (resolved on login)
|
|
375
|
+
const { user, signOut, updatePassword, supabase, appId: contextAppId, selectedOrganisationId } = useUnifiedAuth(); // Get appId from context (resolved on login)
|
|
376
376
|
const {
|
|
377
377
|
selectedOrganisation,
|
|
378
378
|
isContextReady,
|
|
@@ -380,6 +380,47 @@ export function PaceAppLayout({
|
|
|
380
380
|
ensureOrganisationContext,
|
|
381
381
|
isLoading: organisationLoading
|
|
382
382
|
} = useOrganisations();
|
|
383
|
+
// Use useRBAC to get super admin status - it's more reliable than async check
|
|
384
|
+
// Note: isSuperAdmin might be false initially while loading, but that's OK - we'll allow rendering
|
|
385
|
+
// if organisation loading completes or if we're a super admin
|
|
386
|
+
const { isSuperAdmin: isSuperAdminFromRBAC, isLoading: rbacLoading } = useRBAC();
|
|
387
|
+
|
|
388
|
+
// Also check super admin status directly as a fallback (for ADMIN/PORTAL apps)
|
|
389
|
+
// This allows super admins to proceed even if RBAC hasn't loaded yet
|
|
390
|
+
const [isSuperAdminDirect, setIsSuperAdminDirect] = useState<boolean>(false);
|
|
391
|
+
const [isCheckingSuperAdminDirect, setIsCheckingSuperAdminDirect] = useState<boolean>(false);
|
|
392
|
+
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
const checkSuperAdminDirect = async () => {
|
|
395
|
+
if (!user?.id) {
|
|
396
|
+
setIsSuperAdminDirect(false);
|
|
397
|
+
setIsCheckingSuperAdminDirect(false);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Only skip if RBAC already confirmed super admin
|
|
402
|
+
if (isSuperAdminFromRBAC) {
|
|
403
|
+
setIsCheckingSuperAdminDirect(false);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
setIsCheckingSuperAdminDirect(true);
|
|
408
|
+
try {
|
|
409
|
+
const superAdminStatus = await checkSuperAdminApi(user.id);
|
|
410
|
+
setIsSuperAdminDirect(superAdminStatus);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
logger.error('PaceAppLayout', 'Error checking super admin status directly', { userId: user?.id, error });
|
|
413
|
+
setIsSuperAdminDirect(false);
|
|
414
|
+
} finally {
|
|
415
|
+
setIsCheckingSuperAdminDirect(false);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
checkSuperAdminDirect();
|
|
420
|
+
}, [user?.id, isSuperAdminFromRBAC]);
|
|
421
|
+
|
|
422
|
+
// Use direct check if RBAC hasn't loaded yet, otherwise use RBAC result
|
|
423
|
+
const isSuperAdmin = isSuperAdminFromRBAC || isSuperAdminDirect;
|
|
383
424
|
const navigate = useNavigate();
|
|
384
425
|
const location = useLocation();
|
|
385
426
|
|
|
@@ -408,28 +449,25 @@ export function PaceAppLayout({
|
|
|
408
449
|
|
|
409
450
|
// Build scope from resolved values
|
|
410
451
|
// Preserve appId from resolvedScope or fallback to resolvedAppId
|
|
452
|
+
// CRITICAL: Always create a new scope object from primitive values to ensure stable reference
|
|
453
|
+
// This prevents useCan from re-checking permissions when resolvedScope changes reference but values are the same
|
|
454
|
+
const scopeOrgId = resolvedScope?.organisationId || selectedOrganisation?.id || '';
|
|
455
|
+
const scopeEventId = resolvedScope?.eventId || selectedEvent?.event_id || undefined;
|
|
456
|
+
const scopeAppId = resolvedScope?.appId || resolvedAppId || undefined;
|
|
457
|
+
|
|
411
458
|
const scope = useMemo<Scope>(() => {
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
459
|
+
const newScope: Scope = {};
|
|
460
|
+
if (scopeOrgId) {
|
|
461
|
+
newScope.organisationId = scopeOrgId;
|
|
415
462
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (selectedOrganisation?.id) {
|
|
419
|
-
return {
|
|
420
|
-
organisationId: selectedOrganisation.id,
|
|
421
|
-
eventId: selectedEvent?.event_id || undefined,
|
|
422
|
-
appId: resolvedAppId || resolvedScope?.appId || undefined
|
|
423
|
-
};
|
|
463
|
+
if (scopeEventId) {
|
|
464
|
+
newScope.eventId = scopeEventId;
|
|
424
465
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
appId: resolvedAppId || resolvedScope?.appId || undefined
|
|
431
|
-
};
|
|
432
|
-
}, [resolvedScope, selectedOrganisation?.id, selectedEvent?.event_id, resolvedAppId]);
|
|
466
|
+
if (scopeAppId) {
|
|
467
|
+
newScope.appId = scopeAppId;
|
|
468
|
+
}
|
|
469
|
+
return newScope;
|
|
470
|
+
}, [scopeOrgId, scopeEventId, scopeAppId]);
|
|
433
471
|
|
|
434
472
|
// Default navigation items if none provided
|
|
435
473
|
const defaultNavItems: NavigationItem[] = useMemo(() => [
|
|
@@ -460,61 +498,45 @@ export function PaceAppLayout({
|
|
|
460
498
|
}
|
|
461
499
|
// Extract first path segment (base page name)
|
|
462
500
|
const pathSegments = currentPath.slice(1).split('/').filter(Boolean);
|
|
463
|
-
|
|
501
|
+
// Only return 'home' if there's actually a path segment, otherwise return empty string
|
|
502
|
+
// This prevents checking permissions for a non-existent "home" page when the index route is used
|
|
503
|
+
return pathSegments[0] || '';
|
|
464
504
|
}, [location.pathname, pageIdMapping]);
|
|
465
505
|
|
|
466
506
|
// Build permission string in format: operation:page.pageId
|
|
467
507
|
const currentPermission = useMemo<Permission>(() => {
|
|
468
|
-
|
|
469
|
-
|
|
508
|
+
// If enforcePermissions is false, don't check any permission (return empty string)
|
|
509
|
+
// If currentPageId is empty (index route with no path segments), don't check permissions
|
|
510
|
+
if (!enforcePermissions || !currentPageId) {
|
|
511
|
+
return '' as Permission;
|
|
470
512
|
}
|
|
471
513
|
const permissionString = `${currentRoutePermission}:page.${currentPageId}`;
|
|
472
514
|
return permissionString as Permission;
|
|
473
515
|
}, [enforcePermissions, currentRoutePermission, currentPageId]);
|
|
474
516
|
|
|
475
517
|
// Check super admin status before permission enforcement
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
useEffect(() => {
|
|
480
|
-
const checkSuperAdminStatus = async () => {
|
|
481
|
-
if (!user?.id) {
|
|
482
|
-
setIsSuperAdminUser(false);
|
|
483
|
-
setIsCheckingSuperAdmin(false);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
setIsCheckingSuperAdmin(true);
|
|
488
|
-
try {
|
|
489
|
-
const superAdminStatus = await isSuperAdmin(user.id);
|
|
490
|
-
setIsSuperAdminUser(superAdminStatus);
|
|
491
|
-
} catch (error) {
|
|
492
|
-
logger.error('PaceAppLayout', 'Error checking super admin status', { userId: user?.id, error });
|
|
493
|
-
setIsSuperAdminUser(false);
|
|
494
|
-
} finally {
|
|
495
|
-
setIsCheckingSuperAdmin(false);
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
checkSuperAdminStatus();
|
|
500
|
-
}, [user?.id]);
|
|
518
|
+
// Removed duplicate super admin check - using useRBAC hook instead
|
|
519
|
+
// The useRBAC hook provides isSuperAdmin which is more reliable
|
|
501
520
|
|
|
502
521
|
// Use useCan hook for permission checking (standardized approach)
|
|
503
522
|
// Note: The database function already handles super admin bypass, but we check here
|
|
504
523
|
// as an additional safety layer to prevent unnecessary permission checks
|
|
505
524
|
// Pass appName to useCan so it can be passed to isPermitted for PORTAL/ADMIN special case
|
|
525
|
+
// Only check permissions if enforcePermissions is true and we have a valid permission string
|
|
526
|
+
const shouldCheckPermission = enforcePermissions && !!currentPermission && !!currentPageId;
|
|
506
527
|
const { can: canFromHook, isLoading: isCheckingPermission, error: permissionError } = useCan(
|
|
507
528
|
user?.id || '',
|
|
508
529
|
scope,
|
|
509
|
-
currentPermission,
|
|
510
|
-
currentPageId,
|
|
530
|
+
shouldCheckPermission ? currentPermission : ('' as Permission),
|
|
531
|
+
shouldCheckPermission ? currentPageId : '',
|
|
511
532
|
true, // useCache
|
|
512
533
|
appName // Pass appName for PORTAL/ADMIN special case
|
|
513
534
|
);
|
|
514
535
|
|
|
515
536
|
// Permission enforcement state - super admin bypasses all checks
|
|
516
537
|
// This ensures super admins never see permission errors even if useCan hasn't completed
|
|
517
|
-
|
|
538
|
+
// Use combined super admin check (RBAC + direct check)
|
|
539
|
+
const can = isSuperAdmin ? true : canFromHook;
|
|
518
540
|
const hasPermission = enforcePermissions ? can : true;
|
|
519
541
|
|
|
520
542
|
// Handle permission check results with audit logging and callbacks
|
|
@@ -524,19 +546,20 @@ export function PaceAppLayout({
|
|
|
524
546
|
}
|
|
525
547
|
|
|
526
548
|
// Only proceed when permission check is complete (not loading)
|
|
527
|
-
//
|
|
528
|
-
|
|
549
|
+
// Super admin status is checked via useRBAC hook (isSuperAdminFromRBAC)
|
|
550
|
+
// If RBAC is still loading, allow rendering to proceed (optimistic for super admins)
|
|
551
|
+
if (isCheckingPermission) {
|
|
529
552
|
return;
|
|
530
553
|
}
|
|
531
554
|
|
|
532
555
|
// NEW: Phase 1 - Enhanced Security Features
|
|
533
556
|
// Handle strict mode violations - skip for super admins
|
|
534
|
-
if (strictMode && !
|
|
557
|
+
if (strictMode && !isSuperAdmin && !can) {
|
|
535
558
|
logger.error('PaceAppLayout', 'STRICT MODE VIOLATION: User attempted to access protected page without permission', {
|
|
536
559
|
pageName: currentPageId,
|
|
537
560
|
operation: currentRoutePermission,
|
|
538
561
|
userId: user?.id,
|
|
539
|
-
isSuperAdmin:
|
|
562
|
+
isSuperAdmin: isSuperAdmin,
|
|
540
563
|
timestamp: new Date().toISOString()
|
|
541
564
|
});
|
|
542
565
|
|
|
@@ -546,10 +569,10 @@ export function PaceAppLayout({
|
|
|
546
569
|
}
|
|
547
570
|
|
|
548
571
|
// Handle page access denied callback - skip for super admins
|
|
549
|
-
if (!
|
|
572
|
+
if (!isSuperAdmin && !can && onPageAccessDenied) {
|
|
550
573
|
onPageAccessDenied(currentPageId, currentRoutePermission);
|
|
551
574
|
}
|
|
552
|
-
}, [enforcePermissions, can, isCheckingPermission,
|
|
575
|
+
}, [enforcePermissions, can, isCheckingPermission, isSuperAdmin, currentPageId, currentRoutePermission, user?.id, strictMode, auditLog, onPageAccessDenied, onStrictModeViolation]);
|
|
553
576
|
|
|
554
577
|
// Filter navigation items based on permissions
|
|
555
578
|
// Permission filtering is always enabled - users only see navigation items they have permission to access
|
|
@@ -610,8 +633,8 @@ export function PaceAppLayout({
|
|
|
610
633
|
// For super admins, show all items (they bypass permission checks)
|
|
611
634
|
// Gracefully handle RBAC not being initialized (e.g., in tests)
|
|
612
635
|
try {
|
|
613
|
-
const { isSuperAdmin } = await import('../../rbac/api');
|
|
614
|
-
const isSuper = await
|
|
636
|
+
const { isSuperAdmin: checkSuperAdminDynamic } = await import('../../rbac/api');
|
|
637
|
+
const isSuper = await checkSuperAdminDynamic(user.id);
|
|
615
638
|
|
|
616
639
|
if (isSuper) {
|
|
617
640
|
// Super admins see all navigation items
|
|
@@ -825,7 +848,13 @@ export function PaceAppLayout({
|
|
|
825
848
|
// This is critical - we must wait for organisation context before allowing any data access
|
|
826
849
|
// BUT: Allow rendering to proceed if loading is complete, even if user has no organisations (valid state for profile pages)
|
|
827
850
|
// Only block if we're actively loading - once loading completes (success or error), allow rendering
|
|
828
|
-
|
|
851
|
+
// EXCEPTION: Super admins can proceed even during organisation loading (they can access all orgs)
|
|
852
|
+
// Use combined super admin check (RBAC + direct check) to allow super admins to proceed immediately
|
|
853
|
+
// IMPORTANT: If we're still checking super admin status, allow rendering to proceed (optimistic approach)
|
|
854
|
+
// This prevents blocking super admins while their status is being determined
|
|
855
|
+
// Also allow rendering if we already have a selectedOrganisationId (even if organisationLoading is still true)
|
|
856
|
+
// This prevents blank pages when organisation context is available but loading state hasn't cleared yet
|
|
857
|
+
if (user?.id && organisationLoading && !isSuperAdmin && !isCheckingSuperAdminDirect && !rbacLoading && !selectedOrganisationId) {
|
|
829
858
|
return (
|
|
830
859
|
<div className="flex items-center justify-center min-h-screen">
|
|
831
860
|
<div className="text-center">
|
|
@@ -841,10 +870,10 @@ export function PaceAppLayout({
|
|
|
841
870
|
// These pages work with user context only and don't require organisation context
|
|
842
871
|
// The app can check hasValidOrganisationContext() to determine if org context is available for org-specific features
|
|
843
872
|
|
|
844
|
-
// Show loading state while checking permissions
|
|
845
|
-
// Keep loading active until
|
|
846
|
-
//
|
|
847
|
-
if (enforcePermissions &&
|
|
873
|
+
// Show loading state while checking permissions
|
|
874
|
+
// Keep loading active until permission check completes to prevent exposing protected content
|
|
875
|
+
// Super admin status is checked via useRBAC hook (isSuperAdminFromRBAC)
|
|
876
|
+
if (enforcePermissions && isCheckingPermission) {
|
|
848
877
|
return (
|
|
849
878
|
<div className="flex items-center justify-center min-h-screen">
|
|
850
879
|
<div className="text-center">
|
|
@@ -857,7 +886,7 @@ export function PaceAppLayout({
|
|
|
857
886
|
|
|
858
887
|
// Show permission error (only after BOTH checks are complete)
|
|
859
888
|
// Super admins bypass all permission checks, so don't show errors for them
|
|
860
|
-
if (enforcePermissions && permissionError && !
|
|
889
|
+
if (enforcePermissions && permissionError && !isSuperAdmin) {
|
|
861
890
|
return (
|
|
862
891
|
<div className="flex items-center justify-center min-h-screen">
|
|
863
892
|
<div className="text-center">
|
|
@@ -871,7 +900,7 @@ export function PaceAppLayout({
|
|
|
871
900
|
|
|
872
901
|
// Show permission fallback if user lacks permission
|
|
873
902
|
// Only show this if super admin check is complete and user is not a super admin
|
|
874
|
-
if (enforcePermissions && hasPermission === false && !
|
|
903
|
+
if (enforcePermissions && hasPermission === false && !isCheckingSuperAdminDirect && !isSuperAdmin) {
|
|
875
904
|
// NEW: Phase 1 - Use page permission fallback if available
|
|
876
905
|
if (enforcePagePermissions && pagePermissionFallback) {
|
|
877
906
|
return <>{pagePermissionFallback}</>;
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
* - Automatic redirect prevention loops
|
|
112
112
|
*
|
|
113
113
|
* @dependencies
|
|
114
|
-
* - React
|
|
114
|
+
* - React 19+ - Hooks and effects
|
|
115
115
|
* - React Router v6 - Navigation
|
|
116
116
|
* - UnifiedAuthProvider - Authentication
|
|
117
117
|
* - LoginForm component
|
|
@@ -208,7 +208,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
|
|
|
208
208
|
}
|
|
209
209
|
} catch (error) {
|
|
210
210
|
// Service may not be available yet or events not loaded - that's okay
|
|
211
|
-
logger.debug('PaceLoginPage', 'Could not restore persisted event (service may not be ready):', error);
|
|
212
211
|
}
|
|
213
212
|
};
|
|
214
213
|
|
|
@@ -266,7 +265,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
|
|
|
266
265
|
.eq('app_id', appData.id);
|
|
267
266
|
|
|
268
267
|
if (pagesError || !pagesData || pagesData.length === 0) {
|
|
269
|
-
logger.debug('PaceLoginPage', 'No pages configured for app:', appName);
|
|
270
268
|
setAccessError(`You do not have permission to access ${appName}. This application is currently unavailable. Please contact your administrator if you believe you should have access.`);
|
|
271
269
|
setIsCheckingAccess(false);
|
|
272
270
|
return;
|
|
@@ -285,7 +283,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
|
|
|
285
283
|
const organisationId = orgRow?.organisation_id;
|
|
286
284
|
|
|
287
285
|
if (!organisationId) {
|
|
288
|
-
logger.debug('PaceLoginPage', 'User has no organisation access');
|
|
289
286
|
setAccessError(`You do not have permission to access ${appName}. You are not assigned to any organisation. Please contact your administrator.`);
|
|
290
287
|
setIsCheckingAccess(false);
|
|
291
288
|
return;
|
|
@@ -305,8 +302,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
|
|
|
305
302
|
p_page_id: page.page_name // Page name to resolve to UUID
|
|
306
303
|
});
|
|
307
304
|
|
|
308
|
-
logger.debug('PaceLoginPage', 'Permission check for page:', { pageName: page.page_name, hasPermission, error: permError });
|
|
309
|
-
|
|
310
305
|
if (!permError && hasPermission === true) {
|
|
311
306
|
hasAnyAccess = true;
|
|
312
307
|
break;
|
|
@@ -314,14 +309,12 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
|
|
|
314
309
|
}
|
|
315
310
|
|
|
316
311
|
if (hasAnyAccess) {
|
|
317
|
-
logger.debug('PaceLoginPage', 'User has access to app');
|
|
318
312
|
setIsCheckingAccess(false);
|
|
319
313
|
navigate(onSuccessRedirectPath, { replace: true });
|
|
320
314
|
return;
|
|
321
315
|
}
|
|
322
316
|
|
|
323
317
|
// No access - deny
|
|
324
|
-
logger.debug('PaceLoginPage', 'Access denied - no permissions');
|
|
325
318
|
setAccessError(`You do not have permission to access ${appName}. This application is restricted to authorized users only. Please contact your administrator if you believe you should have access.`);
|
|
326
319
|
setIsCheckingAccess(false);
|
|
327
320
|
} catch (error) {
|
|
@@ -55,13 +55,13 @@ vi.mock('../Label', () => ({
|
|
|
55
55
|
|
|
56
56
|
describe('PasswordChangeForm', () => {
|
|
57
57
|
const mockOnSubmit = vi.fn();
|
|
58
|
-
const
|
|
58
|
+
const baseProps: PasswordChangeFormProps = {
|
|
59
59
|
onSubmit: mockOnSubmit
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
describe('Rendering', () => {
|
|
63
63
|
it('renders password change form with all elements', () => {
|
|
64
|
-
render(<PasswordChangeForm {...
|
|
64
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
65
65
|
|
|
66
66
|
expect(screen.getByLabelText('New Password')).toBeInTheDocument();
|
|
67
67
|
expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument();
|
|
@@ -70,14 +70,14 @@ describe('PasswordChangeForm', () => {
|
|
|
70
70
|
|
|
71
71
|
it('renders with custom className', () => {
|
|
72
72
|
const customClass = 'custom-password-change-form';
|
|
73
|
-
const { container } = render(<PasswordChangeForm {...
|
|
73
|
+
const { container } = render(<PasswordChangeForm {...baseProps} className={customClass} />);
|
|
74
74
|
|
|
75
75
|
const form = container.querySelector('form');
|
|
76
76
|
expect(form).toHaveClass(customClass);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it('has proper form structure and accessibility', () => {
|
|
80
|
-
render(<PasswordChangeForm {...
|
|
80
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
81
81
|
|
|
82
82
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
83
83
|
expect(newPasswordInput).toHaveAttribute('type', 'password');
|
|
@@ -97,7 +97,7 @@ describe('PasswordChangeForm', () => {
|
|
|
97
97
|
describe('Form Interaction', () => {
|
|
98
98
|
it('updates new password input value when user types', async () => {
|
|
99
99
|
const user = userEvent.setup();
|
|
100
|
-
render(<PasswordChangeForm {...
|
|
100
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
101
101
|
|
|
102
102
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
103
103
|
await user.type(newPasswordInput, 'newpassword123');
|
|
@@ -107,7 +107,7 @@ describe('PasswordChangeForm', () => {
|
|
|
107
107
|
|
|
108
108
|
it('updates confirm password input value when user types', async () => {
|
|
109
109
|
const user = userEvent.setup();
|
|
110
|
-
render(<PasswordChangeForm {...
|
|
110
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
111
111
|
|
|
112
112
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
113
113
|
await user.type(confirmPasswordInput, 'newpassword123');
|
|
@@ -117,7 +117,7 @@ describe('PasswordChangeForm', () => {
|
|
|
117
117
|
|
|
118
118
|
it('enables submit button when both passwords are provided', async () => {
|
|
119
119
|
const user = userEvent.setup();
|
|
120
|
-
render(<PasswordChangeForm {...
|
|
120
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
121
121
|
|
|
122
122
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
123
123
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -133,7 +133,7 @@ describe('PasswordChangeForm', () => {
|
|
|
133
133
|
|
|
134
134
|
it('disables submit button when new password is empty', async () => {
|
|
135
135
|
const user = userEvent.setup();
|
|
136
|
-
render(<PasswordChangeForm {...
|
|
136
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
137
137
|
|
|
138
138
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
139
139
|
const submitButton = screen.getByRole('button', { name: 'Change Password' });
|
|
@@ -145,7 +145,7 @@ describe('PasswordChangeForm', () => {
|
|
|
145
145
|
|
|
146
146
|
it('disables submit button when confirm password is empty', async () => {
|
|
147
147
|
const user = userEvent.setup();
|
|
148
|
-
render(<PasswordChangeForm {...
|
|
148
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
149
149
|
|
|
150
150
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
151
151
|
const submitButton = screen.getByRole('button', { name: 'Change Password' });
|
|
@@ -159,7 +159,7 @@ describe('PasswordChangeForm', () => {
|
|
|
159
159
|
describe('Form Validation', () => {
|
|
160
160
|
it('shows error when password is too short', async () => {
|
|
161
161
|
const user = userEvent.setup();
|
|
162
|
-
render(<PasswordChangeForm {...
|
|
162
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
163
163
|
|
|
164
164
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
165
165
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -175,7 +175,7 @@ describe('PasswordChangeForm', () => {
|
|
|
175
175
|
|
|
176
176
|
it('shows error when passwords do not match', async () => {
|
|
177
177
|
const user = userEvent.setup();
|
|
178
|
-
render(<PasswordChangeForm {...
|
|
178
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
179
179
|
|
|
180
180
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
181
181
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -191,7 +191,7 @@ describe('PasswordChangeForm', () => {
|
|
|
191
191
|
|
|
192
192
|
it('validates password length before checking match', async () => {
|
|
193
193
|
const user = userEvent.setup();
|
|
194
|
-
render(<PasswordChangeForm {...
|
|
194
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
195
195
|
|
|
196
196
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
197
197
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -207,7 +207,7 @@ describe('PasswordChangeForm', () => {
|
|
|
207
207
|
|
|
208
208
|
it('clears error when form is resubmitted with valid data', async () => {
|
|
209
209
|
const user = userEvent.setup();
|
|
210
|
-
render(<PasswordChangeForm {...
|
|
210
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
211
211
|
|
|
212
212
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
213
213
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -236,7 +236,7 @@ describe('PasswordChangeForm', () => {
|
|
|
236
236
|
const user = userEvent.setup();
|
|
237
237
|
mockOnSubmit.mockResolvedValue({});
|
|
238
238
|
|
|
239
|
-
render(<PasswordChangeForm {...
|
|
239
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
240
240
|
|
|
241
241
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
242
242
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -254,7 +254,7 @@ describe('PasswordChangeForm', () => {
|
|
|
254
254
|
|
|
255
255
|
it('prevents form submission when validation fails', async () => {
|
|
256
256
|
const user = userEvent.setup();
|
|
257
|
-
render(<PasswordChangeForm {...
|
|
257
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
258
258
|
|
|
259
259
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
260
260
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -273,7 +273,7 @@ describe('PasswordChangeForm', () => {
|
|
|
273
273
|
const user = userEvent.setup();
|
|
274
274
|
mockOnSubmit.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
275
275
|
|
|
276
|
-
render(<PasswordChangeForm {...
|
|
276
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
277
277
|
|
|
278
278
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
279
279
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -291,7 +291,7 @@ describe('PasswordChangeForm', () => {
|
|
|
291
291
|
const user = userEvent.setup();
|
|
292
292
|
mockOnSubmit.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
293
293
|
|
|
294
|
-
render(<PasswordChangeForm {...
|
|
294
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
295
295
|
|
|
296
296
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
297
297
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -309,7 +309,7 @@ describe('PasswordChangeForm', () => {
|
|
|
309
309
|
const user = userEvent.setup();
|
|
310
310
|
mockOnSubmit.mockResolvedValue({});
|
|
311
311
|
|
|
312
|
-
render(<PasswordChangeForm {...
|
|
312
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
313
313
|
|
|
314
314
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
315
315
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -329,7 +329,7 @@ describe('PasswordChangeForm', () => {
|
|
|
329
329
|
const user = userEvent.setup();
|
|
330
330
|
mockOnSubmit.mockResolvedValue({ error: { message: 'Test error' } });
|
|
331
331
|
|
|
332
|
-
render(<PasswordChangeForm {...
|
|
332
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
333
333
|
|
|
334
334
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
335
335
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -352,7 +352,7 @@ describe('PasswordChangeForm', () => {
|
|
|
352
352
|
const errorMessage = 'Password change failed';
|
|
353
353
|
mockOnSubmit.mockResolvedValue({ error: { message: errorMessage } });
|
|
354
354
|
|
|
355
|
-
render(<PasswordChangeForm {...
|
|
355
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
356
356
|
|
|
357
357
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
358
358
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -373,7 +373,7 @@ describe('PasswordChangeForm', () => {
|
|
|
373
373
|
const errorMessage = 'Network error';
|
|
374
374
|
mockOnSubmit.mockRejectedValue(new Error(errorMessage));
|
|
375
375
|
|
|
376
|
-
render(<PasswordChangeForm {...
|
|
376
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
377
377
|
|
|
378
378
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
379
379
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -393,7 +393,7 @@ describe('PasswordChangeForm', () => {
|
|
|
393
393
|
const user = userEvent.setup();
|
|
394
394
|
mockOnSubmit.mockRejectedValue('String error');
|
|
395
395
|
|
|
396
|
-
render(<PasswordChangeForm {...
|
|
396
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
397
397
|
|
|
398
398
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
399
399
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -412,7 +412,7 @@ describe('PasswordChangeForm', () => {
|
|
|
412
412
|
const user = userEvent.setup();
|
|
413
413
|
mockOnSubmit.mockResolvedValue({ error: { code: 'INVALID_PASSWORD' } });
|
|
414
414
|
|
|
415
|
-
render(<PasswordChangeForm {...
|
|
415
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
416
416
|
|
|
417
417
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
418
418
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -433,7 +433,7 @@ describe('PasswordChangeForm', () => {
|
|
|
433
433
|
.mockResolvedValueOnce({ error: { message: 'First error' } })
|
|
434
434
|
.mockResolvedValueOnce({});
|
|
435
435
|
|
|
436
|
-
render(<PasswordChangeForm {...
|
|
436
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
437
437
|
|
|
438
438
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
439
439
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -457,7 +457,7 @@ describe('PasswordChangeForm', () => {
|
|
|
457
457
|
|
|
458
458
|
describe('Accessibility', () => {
|
|
459
459
|
it('has proper form labels and associations', () => {
|
|
460
|
-
render(<PasswordChangeForm {...
|
|
460
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
461
461
|
|
|
462
462
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
463
463
|
expect(newPasswordInput).toHaveAttribute('id', 'new-password');
|
|
@@ -472,7 +472,7 @@ describe('PasswordChangeForm', () => {
|
|
|
472
472
|
const user = userEvent.setup();
|
|
473
473
|
mockOnSubmit.mockResolvedValue({ error: { message: 'Test error' } });
|
|
474
474
|
|
|
475
|
-
render(<PasswordChangeForm {...
|
|
475
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
476
476
|
|
|
477
477
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
478
478
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -492,7 +492,7 @@ describe('PasswordChangeForm', () => {
|
|
|
492
492
|
const user = userEvent.setup();
|
|
493
493
|
mockOnSubmit.mockResolvedValue({});
|
|
494
494
|
|
|
495
|
-
render(<PasswordChangeForm {...
|
|
495
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
496
496
|
|
|
497
497
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
498
498
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -510,7 +510,7 @@ describe('PasswordChangeForm', () => {
|
|
|
510
510
|
|
|
511
511
|
describe('Edge Cases', () => {
|
|
512
512
|
it('handles empty password values', () => {
|
|
513
|
-
render(<PasswordChangeForm {...
|
|
513
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
514
514
|
|
|
515
515
|
const submitButton = screen.getByRole('button', { name: 'Change Password' });
|
|
516
516
|
|
|
@@ -519,7 +519,7 @@ describe('PasswordChangeForm', () => {
|
|
|
519
519
|
|
|
520
520
|
it('handles whitespace-only passwords', async () => {
|
|
521
521
|
const user = userEvent.setup();
|
|
522
|
-
render(<PasswordChangeForm {...
|
|
522
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
523
523
|
|
|
524
524
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
525
525
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -538,7 +538,7 @@ describe('PasswordChangeForm', () => {
|
|
|
538
538
|
const longPassword = 'a'.repeat(1000);
|
|
539
539
|
mockOnSubmit.mockResolvedValue({});
|
|
540
540
|
|
|
541
|
-
render(<PasswordChangeForm {...
|
|
541
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
542
542
|
|
|
543
543
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
544
544
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -559,7 +559,7 @@ describe('PasswordChangeForm', () => {
|
|
|
559
559
|
const specialPassword = 'P@ssw0rd!@#$%^&*()_+-=';
|
|
560
560
|
mockOnSubmit.mockResolvedValue({});
|
|
561
561
|
|
|
562
|
-
render(<PasswordChangeForm {...
|
|
562
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
563
563
|
|
|
564
564
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
565
565
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -580,7 +580,7 @@ describe('PasswordChangeForm', () => {
|
|
|
580
580
|
const unicodePassword = 'pássw0rd_测试_пароль';
|
|
581
581
|
mockOnSubmit.mockResolvedValue({});
|
|
582
582
|
|
|
583
|
-
render(<PasswordChangeForm {...
|
|
583
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
584
584
|
|
|
585
585
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
586
586
|
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
|
@@ -600,7 +600,7 @@ describe('PasswordChangeForm', () => {
|
|
|
600
600
|
describe('Performance', () => {
|
|
601
601
|
it('handles rapid input changes efficiently', async () => {
|
|
602
602
|
const user = userEvent.setup();
|
|
603
|
-
render(<PasswordChangeForm {...
|
|
603
|
+
render(<PasswordChangeForm {...baseProps} />);
|
|
604
604
|
|
|
605
605
|
const newPasswordInput = screen.getByLabelText('New Password');
|
|
606
606
|
|