@jmruthers/pace-core 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/cursor-rules/00-pace-core-compliance.mdc +34 -2
- package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
- package/dist/{DataTable-TPTKCX4D.js → DataTable-THFPBKTP.js} +9 -8
- package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
- package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
- package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
- package/dist/{api-MVVQZLJI.js → api-IAGWF3ZG.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/{chunk-SFZUDBL5.js → chunk-2T2IG7T7.js} +70 -56
- package/dist/chunk-2T2IG7T7.js.map +1 -0
- package/dist/{chunk-MMZ7JXPU.js → chunk-6Z7LTB3D.js} +13 -21
- package/dist/{chunk-MMZ7JXPU.js.map → chunk-6Z7LTB3D.js.map} +1 -1
- package/dist/{chunk-6J4GEEJR.js → chunk-CNCQDFLN.js} +53 -27
- package/dist/chunk-CNCQDFLN.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-EHMR7VYL.js → chunk-DWUBLJJM.js} +361 -187
- package/dist/chunk-DWUBLJJM.js.map +1 -0
- package/dist/{chunk-2UOI2FG5.js → chunk-HFZBI76P.js} +4 -4
- package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
- package/dist/{chunk-3XC4CPTD.js → chunk-PQBSKX33.js} +244 -5727
- package/dist/chunk-PQBSKX33.js.map +1 -0
- package/dist/chunk-QRPVRXYT.js +226 -0
- package/dist/chunk-QRPVRXYT.js.map +1 -0
- package/dist/{chunk-24UVZUZG.js → chunk-RWEBCB47.js} +129 -387
- package/dist/chunk-RWEBCB47.js.map +1 -0
- package/dist/{chunk-XWQCNGTQ.js → chunk-YDQHOZNA.js} +173 -79
- package/dist/chunk-YDQHOZNA.js.map +1 -0
- package/dist/{chunk-NECFR5MM.js → chunk-ZNIWI3UC.js} +562 -644
- package/dist/chunk-ZNIWI3UC.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +12 -13
- package/dist/contextValidator-3JNZKUTX.js +9 -0
- package/dist/contextValidator-3JNZKUTX.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +67 -27
- package/dist/rbac/index.js +15 -8
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
- package/dist/utils.js +5 -7
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +14 -16
- package/docs/api/modules.md +3796 -2513
- package/docs/components/context-selector.md +126 -0
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- package/docs/rbac/secure-client-protection.md +330 -0
- package/package.json +3 -3
- package/scripts/audit/core/checks/compliance.cjs +72 -0
- package/scripts/audit/core/checks/dependencies.cjs +559 -28
- package/scripts/audit/core/checks/documentation.cjs +68 -3
- package/scripts/audit/core/checks/environment.cjs +2 -14
- package/scripts/audit/core/checks/error-handling.cjs +47 -6
- package/src/components/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/components/RowComponent.tsx +19 -19
- package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
- package/src/components/Dialog/Dialog.tsx +29 -1
- package/src/components/FileDisplay/FileDisplay.tsx +42 -10
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +44 -45
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +135 -33
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +2 -2
- package/src/components/index.ts +5 -5
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/useAppConfig.ts +15 -30
- package/src/hooks/useFileDisplay.ts +77 -50
- package/src/index.ts +4 -5
- package/src/providers/services/AuthServiceProvider.tsx +17 -7
- package/src/providers/services/EventServiceProvider.tsx +33 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/adapters.tsx +2 -2
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +178 -132
- package/src/rbac/components/PagePermissionGuard.tsx +38 -10
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
- package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
- package/src/rbac/hooks/permissions/useCan.ts +41 -11
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +0 -9
- package/src/rbac/hooks/useRBAC.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.ts +36 -37
- package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
- package/src/rbac/hooks/useResolvedScope.ts +35 -40
- package/src/rbac/hooks/useSecureSupabase.ts +7 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +103 -16
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +1 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
- package/src/rbac/utils/clientSecurity.ts +93 -0
- package/src/rbac/utils/contextValidator.ts +77 -168
- package/src/services/AuthService.ts +39 -7
- package/src/services/EventService.ts +186 -54
- package/src/services/OrganisationService.ts +81 -14
- package/src/services/__tests__/EventService.test.ts +1 -2
- package/src/services/base/BaseService.ts +3 -0
- package/src/utils/dynamic/dynamicUtils.ts +7 -4
- package/dist/chunk-24UVZUZG.js.map +0 -1
- package/dist/chunk-3XC4CPTD.js.map +0 -1
- package/dist/chunk-6J4GEEJR.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-EHMR7VYL.js.map +0 -1
- package/dist/chunk-NECFR5MM.js.map +0 -1
- package/dist/chunk-SFZUDBL5.js.map +0 -1
- package/dist/chunk-XWQCNGTQ.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/InvalidScopeError.md +0 -73
- package/docs/api/classes/Logger.md +0 -178
- package/docs/api/classes/MissingUserContextError.md +0 -66
- package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
- package/docs/api/classes/PermissionDeniedError.md +0 -73
- package/docs/api/classes/RBACAuditManager.md +0 -297
- package/docs/api/classes/RBACCache.md +0 -322
- package/docs/api/classes/RBACEngine.md +0 -171
- package/docs/api/classes/RBACError.md +0 -76
- package/docs/api/classes/RBACNotInitializedError.md +0 -66
- package/docs/api/classes/SecureSupabaseClient.md +0 -163
- package/docs/api/classes/StorageUtils.md +0 -328
- package/docs/api/enums/FileCategory.md +0 -184
- package/docs/api/enums/LogLevel.md +0 -54
- package/docs/api/enums/RBACErrorCode.md +0 -228
- package/docs/api/enums/RPCFunction.md +0 -118
- package/docs/api/interfaces/AddressFieldProps.md +0 -241
- package/docs/api/interfaces/AddressFieldRef.md +0 -94
- package/docs/api/interfaces/AggregateConfig.md +0 -43
- package/docs/api/interfaces/AutocompleteOptions.md +0 -75
- package/docs/api/interfaces/AvatarProps.md +0 -128
- package/docs/api/interfaces/BadgeProps.md +0 -34
- package/docs/api/interfaces/ButtonProps.md +0 -56
- package/docs/api/interfaces/CalendarProps.md +0 -73
- package/docs/api/interfaces/CardProps.md +0 -69
- package/docs/api/interfaces/ColorPalette.md +0 -7
- package/docs/api/interfaces/ColorShade.md +0 -66
- package/docs/api/interfaces/ComplianceResult.md +0 -30
- package/docs/api/interfaces/DataAccessRecord.md +0 -96
- package/docs/api/interfaces/DataRecord.md +0 -11
- package/docs/api/interfaces/DataTableAction.md +0 -252
- package/docs/api/interfaces/DataTableColumn.md +0 -504
- package/docs/api/interfaces/DataTableProps.md +0 -625
- package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
- package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
- package/docs/api/interfaces/DatabaseIssue.md +0 -41
- package/docs/api/interfaces/EmptyStateConfig.md +0 -61
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
- package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
- package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
- package/docs/api/interfaces/EventAppRoleData.md +0 -71
- package/docs/api/interfaces/ExportColumn.md +0 -90
- package/docs/api/interfaces/ExportOptions.md +0 -126
- package/docs/api/interfaces/FileDisplayProps.md +0 -249
- package/docs/api/interfaces/FileMetadata.md +0 -129
- package/docs/api/interfaces/FileReference.md +0 -118
- package/docs/api/interfaces/FileSizeLimits.md +0 -7
- package/docs/api/interfaces/FileUploadOptions.md +0 -139
- package/docs/api/interfaces/FileUploadProps.md +0 -296
- package/docs/api/interfaces/FooterProps.md +0 -107
- package/docs/api/interfaces/FormFieldProps.md +0 -166
- package/docs/api/interfaces/FormProps.md +0 -113
- package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
- package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
- package/docs/api/interfaces/InputProps.md +0 -56
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -187
- package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
- package/docs/api/interfaces/NavigationContextType.md +0 -164
- package/docs/api/interfaces/NavigationGuardProps.md +0 -139
- package/docs/api/interfaces/NavigationItem.md +0 -120
- package/docs/api/interfaces/NavigationMenuProps.md +0 -221
- package/docs/api/interfaces/NavigationProviderProps.md +0 -117
- package/docs/api/interfaces/Organisation.md +0 -140
- package/docs/api/interfaces/OrganisationContextType.md +0 -388
- package/docs/api/interfaces/OrganisationMembership.md +0 -140
- package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
- package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
- package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
- package/docs/api/interfaces/PageAccessRecord.md +0 -85
- package/docs/api/interfaces/PagePermissionContextType.md +0 -140
- package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
- package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
- package/docs/api/interfaces/PaletteData.md +0 -41
- package/docs/api/interfaces/ParsedAddress.md +0 -120
- package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
- package/docs/api/interfaces/ProgressProps.md +0 -42
- package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
- package/docs/api/interfaces/QuickFix.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
- package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
- package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
- package/docs/api/interfaces/RBACConfig.md +0 -133
- package/docs/api/interfaces/RBACContext.md +0 -52
- package/docs/api/interfaces/RBACLogger.md +0 -112
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
- package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
- package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
- package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
- package/docs/api/interfaces/RBACResult.md +0 -58
- package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
- package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
- package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
- package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
- package/docs/api/interfaces/RBACRolesListParams.md +0 -52
- package/docs/api/interfaces/RBACRolesListResult.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
- package/docs/api/interfaces/ResourcePermissions.md +0 -155
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
- package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
- package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
- package/docs/api/interfaces/RoleManagementResult.md +0 -52
- package/docs/api/interfaces/RouteAccessRecord.md +0 -107
- package/docs/api/interfaces/RouteConfig.md +0 -134
- package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
- package/docs/api/interfaces/SecureDataContextType.md +0 -168
- package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
- package/docs/api/interfaces/SetupIssue.md +0 -41
- package/docs/api/interfaces/StorageConfig.md +0 -41
- package/docs/api/interfaces/StorageFileInfo.md +0 -74
- package/docs/api/interfaces/StorageFileMetadata.md +0 -151
- package/docs/api/interfaces/StorageListOptions.md +0 -99
- package/docs/api/interfaces/StorageListResult.md +0 -41
- package/docs/api/interfaces/StorageUploadOptions.md +0 -101
- package/docs/api/interfaces/StorageUploadResult.md +0 -63
- package/docs/api/interfaces/StorageUrlOptions.md +0 -60
- package/docs/api/interfaces/StyleImport.md +0 -19
- package/docs/api/interfaces/SwitchProps.md +0 -34
- package/docs/api/interfaces/TabsContentProps.md +0 -9
- package/docs/api/interfaces/TabsListProps.md +0 -9
- package/docs/api/interfaces/TabsProps.md +0 -9
- package/docs/api/interfaces/TabsTriggerProps.md +0 -50
- package/docs/api/interfaces/TextareaProps.md +0 -53
- package/docs/api/interfaces/ToastActionElement.md +0 -12
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
- package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
- package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
- package/docs/api/interfaces/UserEventAccess.md +0 -121
- package/docs/api/interfaces/UserMenuProps.md +0 -88
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -423
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
- package/src/components/OrganisationSelector/index.ts +0 -9
- /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-THFPBKTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
- /package/dist/{api-MVVQZLJI.js.map → api-IAGWF3ZG.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-2UOI2FG5.js.map → chunk-HFZBI76P.js.map} +0 -0
- /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
|
@@ -43,10 +43,18 @@ export function useCan(
|
|
|
43
43
|
precomputedSuperAdmin: boolean | null = null,
|
|
44
44
|
appName?: string,
|
|
45
45
|
) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// CRITICAL FIX: Initialize isSuperAdmin from precomputed value immediately
|
|
47
|
+
// This prevents permission checks from running when we already know the user is a super admin
|
|
48
|
+
// If precomputedSuperAdmin is true, we can immediately set can=true and isLoading=false
|
|
49
49
|
const [isSuperAdmin, setIsSuperAdmin] = useState<boolean | null>(precomputedSuperAdmin ?? null);
|
|
50
|
+
|
|
51
|
+
// For super admins, immediately grant permissions without waiting
|
|
52
|
+
const initialCan = precomputedSuperAdmin === true ? true : false;
|
|
53
|
+
const initialIsLoading = precomputedSuperAdmin === true ? false : true;
|
|
54
|
+
|
|
55
|
+
const [can, setCan] = useState<boolean>(initialCan);
|
|
56
|
+
const [isLoading, setIsLoading] = useState<boolean>(initialIsLoading);
|
|
57
|
+
const [error, setError] = useState<Error | null>(null);
|
|
50
58
|
|
|
51
59
|
// Validate scope parameter - handle undefined/null scope gracefully
|
|
52
60
|
const isValidScope = scope && typeof scope === 'object';
|
|
@@ -54,6 +62,19 @@ export function useCan(
|
|
|
54
62
|
const eventId = isValidScope ? scope.eventId : undefined;
|
|
55
63
|
const appId = isValidScope ? scope.appId : undefined;
|
|
56
64
|
|
|
65
|
+
// CRITICAL FIX: Immediately update state when precomputedSuperAdmin changes to true
|
|
66
|
+
// This ensures super admins get immediate access without waiting for permission checks
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (precomputedSuperAdmin === true && isSuperAdmin !== true) {
|
|
69
|
+
setIsSuperAdmin(true);
|
|
70
|
+
setCan(true);
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
setError(null);
|
|
73
|
+
} else if (precomputedSuperAdmin === false && isSuperAdmin !== false) {
|
|
74
|
+
setIsSuperAdmin(false);
|
|
75
|
+
}
|
|
76
|
+
}, [precomputedSuperAdmin, isSuperAdmin]);
|
|
77
|
+
|
|
57
78
|
// Check super-admin status - super admins bypass organisation context requirements
|
|
58
79
|
// PERFORMANCE OPTIMIZATION: Use precomputed value directly - no duplicate checks
|
|
59
80
|
// Callers must check super admin once and pass the result (null if not checked yet)
|
|
@@ -93,6 +114,12 @@ export function useCan(
|
|
|
93
114
|
});
|
|
94
115
|
}
|
|
95
116
|
setIsSuperAdmin(isSuper);
|
|
117
|
+
// If super admin, immediately grant permissions
|
|
118
|
+
if (isSuper) {
|
|
119
|
+
setCan(true);
|
|
120
|
+
setIsLoading(false);
|
|
121
|
+
setError(null);
|
|
122
|
+
}
|
|
96
123
|
}
|
|
97
124
|
} catch (err) {
|
|
98
125
|
if (!cancelled) {
|
|
@@ -111,9 +138,6 @@ export function useCan(
|
|
|
111
138
|
return () => {
|
|
112
139
|
cancelled = true;
|
|
113
140
|
};
|
|
114
|
-
} else {
|
|
115
|
-
// Precomputed value provided (true/false) - use it directly, no check needed
|
|
116
|
-
setIsSuperAdmin(precomputedSuperAdmin);
|
|
117
141
|
}
|
|
118
142
|
}, [userId, precomputedSuperAdmin]);
|
|
119
143
|
|
|
@@ -150,6 +174,7 @@ export function useCan(
|
|
|
150
174
|
const lastPermissionRef = useRef<Permission | null>(null);
|
|
151
175
|
const lastPageIdRef = useRef<UUID | undefined | null>(null);
|
|
152
176
|
const lastUseCacheRef = useRef<boolean | null>(null);
|
|
177
|
+
const lastIsSuperAdminRef = useRef<boolean | null>(null);
|
|
153
178
|
|
|
154
179
|
// Create a stable scope object for comparison
|
|
155
180
|
const stableScope = useMemo(() => {
|
|
@@ -171,13 +196,18 @@ export function useCan(
|
|
|
171
196
|
const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
|
|
172
197
|
|
|
173
198
|
// Only run if something has actually changed
|
|
199
|
+
// CRITICAL: Also check if isSuperAdmin changed - super admins bypass all checks
|
|
200
|
+
const isSuperAdminChanged = lastIsSuperAdminRef.current !== isSuperAdmin;
|
|
201
|
+
|
|
174
202
|
if (
|
|
175
203
|
lastUserIdRef.current !== userId ||
|
|
176
204
|
scopeChanged ||
|
|
177
205
|
lastPermissionRef.current !== permission ||
|
|
178
206
|
lastPageIdRef.current !== pageId ||
|
|
179
|
-
lastUseCacheRef.current !== useCache
|
|
207
|
+
lastUseCacheRef.current !== useCache ||
|
|
208
|
+
isSuperAdminChanged
|
|
180
209
|
) {
|
|
210
|
+
lastIsSuperAdminRef.current = isSuperAdmin;
|
|
181
211
|
lastUserIdRef.current = userId;
|
|
182
212
|
prevScopeRef.current = stableScope;
|
|
183
213
|
lastPermissionRef.current = permission;
|
|
@@ -265,8 +295,8 @@ export function useCan(
|
|
|
265
295
|
// Note: isPermittedCached doesn't support precomputedSuperAdmin, but the check will be cached
|
|
266
296
|
// If we know user is NOT super admin (isSuperAdmin === false), pass false to skip the check
|
|
267
297
|
const result = useCache
|
|
268
|
-
? await isPermittedCached({ userId, scope: validScope, permission, pageId },
|
|
269
|
-
: await isPermitted({ userId, scope: validScope, permission, pageId },
|
|
298
|
+
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
|
|
299
|
+
: await isPermitted({ userId, scope: validScope, permission, pageId }, appName, isSuperAdmin === false ? false : null);
|
|
270
300
|
|
|
271
301
|
setCan(result);
|
|
272
302
|
} catch (err) {
|
|
@@ -325,8 +355,8 @@ export function useCan(
|
|
|
325
355
|
};
|
|
326
356
|
|
|
327
357
|
const result = useCache
|
|
328
|
-
? await isPermittedCached({ userId, scope: validScope, permission, pageId },
|
|
329
|
-
: await isPermitted({ userId, scope: validScope, permission, pageId },
|
|
358
|
+
? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
|
|
359
|
+
: await isPermitted({ userId, scope: validScope, permission, pageId }, appName, null);
|
|
330
360
|
|
|
331
361
|
setCan(result);
|
|
332
362
|
} catch (err) {
|
|
@@ -59,7 +59,7 @@ export function useHasAllPermissions(
|
|
|
59
59
|
for (const permission of permissions) {
|
|
60
60
|
const result = useCache
|
|
61
61
|
? await isPermittedCached({ userId, scope, permission })
|
|
62
|
-
: await isPermitted({ userId, scope, permission }
|
|
62
|
+
: await isPermitted({ userId, scope, permission });
|
|
63
63
|
|
|
64
64
|
if (!result) {
|
|
65
65
|
hasAllPermissions = false;
|
|
@@ -59,7 +59,7 @@ export function useHasAnyPermission(
|
|
|
59
59
|
for (const permission of permissions) {
|
|
60
60
|
const result = useCache
|
|
61
61
|
? await isPermittedCached({ userId, scope, permission })
|
|
62
|
-
: await isPermitted({ userId, scope, permission }
|
|
62
|
+
: await isPermitted({ userId, scope, permission });
|
|
63
63
|
|
|
64
64
|
if (result) {
|
|
65
65
|
hasAnyPermission = true;
|
|
@@ -66,7 +66,7 @@ export function useMultiplePermissions(
|
|
|
66
66
|
for (const permission of permissions) {
|
|
67
67
|
const result = useCache
|
|
68
68
|
? await isPermittedCached({ userId, scope, permission })
|
|
69
|
-
: await isPermitted({ userId, scope, permission }
|
|
69
|
+
: await isPermitted({ userId, scope, permission });
|
|
70
70
|
permissionResults[permission] = result;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -126,7 +126,6 @@ describe('useCan Hook', () => {
|
|
|
126
126
|
permission: mockPermission,
|
|
127
127
|
pageId: mockPageId
|
|
128
128
|
},
|
|
129
|
-
undefined,
|
|
130
129
|
undefined
|
|
131
130
|
);
|
|
132
131
|
expect(mockIsPermitted).not.toHaveBeenCalled();
|
|
@@ -151,7 +150,6 @@ describe('useCan Hook', () => {
|
|
|
151
150
|
pageId: mockPageId
|
|
152
151
|
},
|
|
153
152
|
undefined,
|
|
154
|
-
undefined,
|
|
155
153
|
false
|
|
156
154
|
);
|
|
157
155
|
expect(mockIsPermittedCached).not.toHaveBeenCalled();
|
|
@@ -191,7 +189,6 @@ describe('useCan Hook', () => {
|
|
|
191
189
|
pageId: 'custom-page'
|
|
192
190
|
},
|
|
193
191
|
undefined,
|
|
194
|
-
undefined,
|
|
195
192
|
false
|
|
196
193
|
);
|
|
197
194
|
});
|
|
@@ -214,7 +211,6 @@ describe('useCan Hook', () => {
|
|
|
214
211
|
pageId: undefined
|
|
215
212
|
},
|
|
216
213
|
undefined,
|
|
217
|
-
undefined,
|
|
218
214
|
false
|
|
219
215
|
);
|
|
220
216
|
});
|
|
@@ -257,7 +253,6 @@ describe('useCan Hook', () => {
|
|
|
257
253
|
pageId: undefined
|
|
258
254
|
},
|
|
259
255
|
undefined,
|
|
260
|
-
undefined,
|
|
261
256
|
false
|
|
262
257
|
);
|
|
263
258
|
});
|
|
@@ -299,7 +294,6 @@ describe('useCan Hook', () => {
|
|
|
299
294
|
pageId: undefined
|
|
300
295
|
},
|
|
301
296
|
undefined,
|
|
302
|
-
undefined,
|
|
303
297
|
false
|
|
304
298
|
);
|
|
305
299
|
});
|
|
@@ -340,7 +334,6 @@ describe('useCan Hook', () => {
|
|
|
340
334
|
pageId: undefined
|
|
341
335
|
},
|
|
342
336
|
undefined,
|
|
343
|
-
undefined,
|
|
344
337
|
false
|
|
345
338
|
);
|
|
346
339
|
});
|
|
@@ -548,7 +541,6 @@ describe('useCan Hook', () => {
|
|
|
548
541
|
permission: mockPermission,
|
|
549
542
|
pageId: mockPageId
|
|
550
543
|
},
|
|
551
|
-
undefined,
|
|
552
544
|
undefined
|
|
553
545
|
);
|
|
554
546
|
});
|
|
@@ -637,7 +629,6 @@ describe('useCan Hook', () => {
|
|
|
637
629
|
pageId: undefined
|
|
638
630
|
},
|
|
639
631
|
undefined,
|
|
640
|
-
undefined,
|
|
641
632
|
false
|
|
642
633
|
);
|
|
643
634
|
});
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getAccessLevel,
|
|
20
20
|
resolveAppContext,
|
|
21
21
|
getRoleContext,
|
|
22
|
+
getPageScopeType,
|
|
22
23
|
} from '../api';
|
|
23
24
|
import { getRBACLogger } from '../config';
|
|
24
25
|
import { ContextValidator } from '../utils/contextValidator';
|
|
@@ -58,7 +59,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
58
59
|
session,
|
|
59
60
|
supabase,
|
|
60
61
|
appName,
|
|
61
|
-
appConfig,
|
|
62
62
|
appId: contextAppId,
|
|
63
63
|
selectedOrganisation,
|
|
64
64
|
isContextReady: orgContextReady,
|
|
@@ -94,30 +94,21 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// Build initial scope from available context
|
|
97
|
-
//
|
|
98
|
-
// For org-required apps: use selectedOrganisation.id
|
|
97
|
+
// Scope is now page-level only - use whatever context is available
|
|
99
98
|
const initialScope: Scope = {
|
|
100
|
-
organisationId:
|
|
101
|
-
? (selectedEvent?.organisation_id || selectedOrganisation?.id)
|
|
102
|
-
: selectedOrganisation?.id,
|
|
99
|
+
organisationId: selectedEvent?.organisation_id || selectedOrganisation?.id || undefined,
|
|
103
100
|
eventId: selectedEvent?.event_id || undefined,
|
|
104
101
|
appId: undefined
|
|
105
102
|
};
|
|
106
103
|
|
|
107
|
-
// Check if context is ready using ContextValidator
|
|
108
|
-
const contextReady = ContextValidator.isContextReady(
|
|
109
|
-
initialScope,
|
|
110
|
-
appConfig,
|
|
111
|
-
appName,
|
|
112
|
-
!!selectedEvent,
|
|
113
|
-
!!selectedOrganisation
|
|
114
|
-
);
|
|
115
|
-
|
|
116
104
|
// PORTAL/ADMIN special case: context is always ready
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
105
|
+
// For other apps, we need at least one context (org or event) for page-level scope validation
|
|
106
|
+
if (appName !== 'PORTAL' && appName !== 'ADMIN') {
|
|
107
|
+
if (!selectedOrganisation && !selectedEvent) {
|
|
108
|
+
// Wait for context to be available
|
|
109
|
+
setIsLoading(true);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
121
112
|
}
|
|
122
113
|
|
|
123
114
|
setIsLoading(true);
|
|
@@ -135,14 +126,8 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
135
126
|
// For PORTAL/ADMIN apps, allow access even if hasAccess is false (users can view their own profile, super admins have global access)
|
|
136
127
|
if (!resolved) {
|
|
137
128
|
if (appName === 'PORTAL' || appName === 'ADMIN') {
|
|
138
|
-
// For PORTAL/ADMIN,
|
|
139
|
-
|
|
140
|
-
const { getAppConfigByName } = await import('../api');
|
|
141
|
-
await getAppConfigByName(appName);
|
|
142
|
-
// We can't get appId from config, but that's OK - use contextAppId or proceed without
|
|
143
|
-
} catch (err) {
|
|
144
|
-
// Proceed without appId for page-level permissions
|
|
145
|
-
}
|
|
129
|
+
// For PORTAL/ADMIN, proceed without appId - it's optional for these apps
|
|
130
|
+
// Use contextAppId if available
|
|
146
131
|
} else {
|
|
147
132
|
throw new Error(`User does not have access to app "${appName}"`);
|
|
148
133
|
}
|
|
@@ -180,11 +165,25 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
180
165
|
appId: appId || contextAppId,
|
|
181
166
|
};
|
|
182
167
|
|
|
183
|
-
// Resolve
|
|
184
|
-
//
|
|
185
|
-
|
|
168
|
+
// Resolve scope based on page-level scope type
|
|
169
|
+
// If pageId is provided, use its scope type; otherwise default to 'organisation'
|
|
170
|
+
let pageScopeType: 'event' | 'organisation' | 'both' = 'organisation';
|
|
171
|
+
if (pageId && scope.appId) {
|
|
172
|
+
try {
|
|
173
|
+
pageScopeType = await getPageScopeType(pageId, scope.appId, appName);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
logger.warn('[useRBAC] Failed to get page scope type, defaulting to organisation', {
|
|
176
|
+
pageId,
|
|
177
|
+
error: error instanceof Error ? error.message : String(error)
|
|
178
|
+
});
|
|
179
|
+
// Default to organisation scope on error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Resolve required context using page-level scope type
|
|
184
|
+
const validation = await ContextValidator.resolveScopeForPage(
|
|
186
185
|
scope,
|
|
187
|
-
|
|
186
|
+
pageScopeType,
|
|
188
187
|
appName,
|
|
189
188
|
supabase || null
|
|
190
189
|
);
|
|
@@ -196,11 +195,11 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
196
195
|
const resolvedScope = validation.resolvedScope;
|
|
197
196
|
setCurrentScope(resolvedScope);
|
|
198
197
|
|
|
199
|
-
//
|
|
198
|
+
// API calls no longer need appConfig (scope is page-level)
|
|
200
199
|
const [map, roleContext, accessLevel] = await Promise.all([
|
|
201
|
-
getPermissionMap({ userId: user.id as UUID, scope: resolvedScope }
|
|
202
|
-
getRoleContext({ userId: user.id as UUID, scope: resolvedScope }
|
|
203
|
-
getAccessLevel({ userId: user.id as UUID, scope: resolvedScope }
|
|
200
|
+
getPermissionMap({ userId: user.id as UUID, scope: resolvedScope }),
|
|
201
|
+
getRoleContext({ userId: user.id as UUID, scope: resolvedScope }),
|
|
202
|
+
getAccessLevel({ userId: user.id as UUID, scope: resolvedScope }),
|
|
204
203
|
]);
|
|
205
204
|
|
|
206
205
|
setPermissionMap(map);
|
|
@@ -225,7 +224,7 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
225
224
|
} finally {
|
|
226
225
|
setIsLoading(false);
|
|
227
226
|
}
|
|
228
|
-
}, [appName, logger, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user, eventLoading,
|
|
227
|
+
}, [appName, logger, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user, eventLoading, orgContextReady, orgLoading]);
|
|
229
228
|
|
|
230
229
|
const hasGlobalPermission = useCallback(
|
|
231
230
|
(permission: string): boolean => {
|
|
@@ -254,7 +253,7 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
254
253
|
|
|
255
254
|
useEffect(() => {
|
|
256
255
|
loadRBACContext();
|
|
257
|
-
}, [loadRBACContext, appName,
|
|
256
|
+
}, [loadRBACContext, appName, eventLoading, selectedEvent?.event_id, user, session, selectedOrganisation?.id, orgContextReady, orgLoading]);
|
|
258
257
|
|
|
259
258
|
return {
|
|
260
259
|
user,
|
|
@@ -24,14 +24,24 @@ vi.mock('../../utils/app/appNameResolver', () => ({
|
|
|
24
24
|
getCurrentAppName: vi.fn(),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
|
+
vi.mock('../utils/contextValidator', () => ({
|
|
28
|
+
ContextValidator: {
|
|
29
|
+
resolveScopeForPage: vi.fn(),
|
|
30
|
+
deriveOrgFromEvent: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
27
34
|
import { createScopeFromEvent, getOrganisationFromEvent } from '../utils/eventContext';
|
|
28
35
|
import { getCurrentAppName } from '../../utils/app/appNameResolver';
|
|
29
36
|
import { createMockSupabaseClient } from '../../__tests__/helpers/supabaseMock';
|
|
37
|
+
import { ContextValidator } from '../utils/contextValidator';
|
|
38
|
+
import { OrganisationContextRequiredError } from '../types';
|
|
30
39
|
|
|
31
40
|
describe('useResolvedScope Hook', () => {
|
|
32
41
|
const mockCreateScopeFromEvent = vi.mocked(createScopeFromEvent);
|
|
33
42
|
const mockGetOrganisationFromEvent = vi.mocked(getOrganisationFromEvent);
|
|
34
43
|
const mockGetCurrentAppName = vi.mocked(getCurrentAppName);
|
|
44
|
+
const mockContextValidator = vi.mocked(ContextValidator);
|
|
35
45
|
|
|
36
46
|
let mockSupabase: SupabaseClient<Database>;
|
|
37
47
|
let sharedMockQuery: any;
|
|
@@ -55,11 +65,32 @@ describe('useResolvedScope Hook', () => {
|
|
|
55
65
|
mockSupabase = {
|
|
56
66
|
from: vi.fn().mockReturnValue(sharedMockQuery),
|
|
57
67
|
rpc: vi.fn(),
|
|
68
|
+
auth: {
|
|
69
|
+
getSession: vi.fn().mockResolvedValue({
|
|
70
|
+
data: { session: { access_token: 'test-token' } },
|
|
71
|
+
error: null
|
|
72
|
+
})
|
|
73
|
+
}
|
|
58
74
|
} as any;
|
|
59
75
|
|
|
60
76
|
mockGetCurrentAppName.mockReturnValue('test-app');
|
|
61
77
|
// Default mock for getOrganisationFromEvent
|
|
62
78
|
mockGetOrganisationFromEvent.mockResolvedValue(null);
|
|
79
|
+
// Default mock for ContextValidator - fails validation when no orgId for organisation scope
|
|
80
|
+
mockContextValidator.resolveScopeForPage.mockImplementation(async (scope, scopeType) => {
|
|
81
|
+
if (!scope.organisationId && scopeType === 'organisation') {
|
|
82
|
+
return {
|
|
83
|
+
isValid: false,
|
|
84
|
+
resolvedScope: null,
|
|
85
|
+
error: new OrganisationContextRequiredError()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
isValid: true,
|
|
90
|
+
resolvedScope: scope,
|
|
91
|
+
error: null
|
|
92
|
+
};
|
|
93
|
+
});
|
|
63
94
|
});
|
|
64
95
|
|
|
65
96
|
afterEach(() => {
|
|
@@ -204,6 +235,13 @@ describe('useResolvedScope Hook', () => {
|
|
|
204
235
|
// Set up mock implementation
|
|
205
236
|
// Note: Don't clear all mocks here as it would clear the getOrganisationFromEvent mock
|
|
206
237
|
mockGetOrganisationFromEvent.mockResolvedValue('org-456');
|
|
238
|
+
// Ensure ContextValidator fails validation for this test (no orgId, but has eventId)
|
|
239
|
+
// This will cause the hook to return scope with just eventId
|
|
240
|
+
mockContextValidator.resolveScopeForPage.mockResolvedValue({
|
|
241
|
+
isValid: false,
|
|
242
|
+
resolvedScope: null,
|
|
243
|
+
error: new OrganisationContextRequiredError()
|
|
244
|
+
});
|
|
207
245
|
(mockSupabase.from as any).mockImplementation((table: string) => {
|
|
208
246
|
if (table === 'event') {
|
|
209
247
|
return eventQueryBuilder;
|
|
@@ -223,39 +261,37 @@ describe('useResolvedScope Hook', () => {
|
|
|
223
261
|
);
|
|
224
262
|
|
|
225
263
|
// Wait for async resolution to complete
|
|
264
|
+
// The hook should set resolvedScope to eventScope when validation fails but eventId exists
|
|
226
265
|
await waitFor(
|
|
227
266
|
() => {
|
|
228
267
|
expect(result.current.isLoading).toBe(false);
|
|
229
268
|
},
|
|
230
|
-
{ timeout: 3000 }
|
|
269
|
+
{ timeout: 3000, interval: 10 }
|
|
231
270
|
);
|
|
232
271
|
|
|
233
|
-
//
|
|
234
|
-
// However, the hook requires organisation context for event-required apps
|
|
235
|
-
// Skip this test as it's testing invalid state (event without org context)
|
|
236
|
-
if (result.current.error) {
|
|
237
|
-
// Expected: Organisation context is required even when deriving from event
|
|
238
|
-
expect(result.current.error.message).toContain('Organisation context is required');
|
|
239
|
-
return; // Test expects this to work, but it's actually invalid state
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Force rerender to pick up ref update
|
|
272
|
+
// Force rerender to pick up ref update (refs don't trigger re-renders)
|
|
243
273
|
rerender();
|
|
244
274
|
|
|
245
|
-
// Wait for stable scope ref to update
|
|
275
|
+
// Wait for stable scope ref to update (happens in useEffect after state update)
|
|
276
|
+
// The scope will have eventId and appId, but no organisationId
|
|
246
277
|
await waitFor(
|
|
247
278
|
() => {
|
|
279
|
+
// Hook should return scope with eventId when validation fails but eventId is present
|
|
248
280
|
expect(result.current.resolvedScope).not.toBeNull();
|
|
249
281
|
},
|
|
250
|
-
{ timeout:
|
|
282
|
+
{ timeout: 2000, interval: 10 }
|
|
251
283
|
);
|
|
252
284
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
285
|
+
// Hook should return scope with eventId even if org derivation hasn't happened yet
|
|
286
|
+
// The organisation will be derived during permission checks
|
|
287
|
+
// When event is provided but validation fails, hook returns scope with just eventId (no error)
|
|
288
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
289
|
+
if (result.current.resolvedScope) {
|
|
290
|
+
expect(result.current.resolvedScope.eventId).toBe('event-123');
|
|
291
|
+
expect(result.current.resolvedScope.appId).toBe('app-123');
|
|
292
|
+
// organisationId may be undefined - will be derived during permission checks
|
|
293
|
+
expect(result.current.error).toBeNull(); // No error when event is provided
|
|
294
|
+
}
|
|
259
295
|
});
|
|
260
296
|
|
|
261
297
|
it('handles no context available', async () => {
|
|
@@ -497,10 +533,20 @@ describe('useResolvedScope Hook', () => {
|
|
|
497
533
|
|
|
498
534
|
describe('Error Handling', () => {
|
|
499
535
|
it('handles error when event scope resolution fails', async () => {
|
|
500
|
-
|
|
501
|
-
|
|
536
|
+
// Ensure appId is resolved so scope is valid
|
|
537
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
538
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
539
|
+
error: null,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Ensure ContextValidator fails validation (no orgId)
|
|
543
|
+
mockContextValidator.resolveScopeForPage.mockResolvedValue({
|
|
544
|
+
isValid: false,
|
|
545
|
+
resolvedScope: null,
|
|
546
|
+
error: new OrganisationContextRequiredError()
|
|
547
|
+
});
|
|
502
548
|
|
|
503
|
-
const { result } = renderHook(() =>
|
|
549
|
+
const { result, rerender } = renderHook(() =>
|
|
504
550
|
useResolvedScope({
|
|
505
551
|
supabase: mockSupabase,
|
|
506
552
|
selectedOrganisationId: null,
|
|
@@ -515,22 +561,43 @@ describe('useResolvedScope Hook', () => {
|
|
|
515
561
|
{ timeout: 2000 }
|
|
516
562
|
);
|
|
517
563
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
564
|
+
// Force rerender to pick up ref update (refs don't trigger re-renders)
|
|
565
|
+
rerender();
|
|
566
|
+
|
|
567
|
+
// Wait for stable scope ref to update (happens in useEffect after state update)
|
|
568
|
+
await waitFor(
|
|
569
|
+
() => {
|
|
570
|
+
// When event is provided but validation fails, hook returns scope with just eventId
|
|
571
|
+
// appId must be resolved for scope to be valid
|
|
572
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
573
|
+
},
|
|
574
|
+
{ timeout: 2000, interval: 10 }
|
|
523
575
|
);
|
|
576
|
+
|
|
577
|
+
// When event is provided but validation fails, hook returns scope with just eventId
|
|
578
|
+
// (no error - org will be derived during permission checks)
|
|
579
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
580
|
+
if (result.current.resolvedScope) {
|
|
581
|
+
expect(result.current.resolvedScope.eventId).toBe('event-123');
|
|
582
|
+
expect(result.current.error).toBeNull();
|
|
583
|
+
}
|
|
524
584
|
});
|
|
525
585
|
|
|
526
586
|
it('handles error when createScopeFromEvent throws', async () => {
|
|
527
|
-
//
|
|
587
|
+
// Ensure appId is resolved so scope is valid
|
|
528
588
|
sharedMockQuery.single.mockResolvedValue({
|
|
529
|
-
data:
|
|
530
|
-
error:
|
|
589
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
590
|
+
error: null,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Ensure ContextValidator fails validation (no orgId)
|
|
594
|
+
mockContextValidator.resolveScopeForPage.mockResolvedValue({
|
|
595
|
+
isValid: false,
|
|
596
|
+
resolvedScope: null,
|
|
597
|
+
error: new OrganisationContextRequiredError()
|
|
531
598
|
});
|
|
532
599
|
|
|
533
|
-
const { result } = renderHook(() =>
|
|
600
|
+
const { result, rerender } = renderHook(() =>
|
|
534
601
|
useResolvedScope({
|
|
535
602
|
supabase: mockSupabase,
|
|
536
603
|
selectedOrganisationId: null,
|
|
@@ -545,10 +612,28 @@ describe('useResolvedScope Hook', () => {
|
|
|
545
612
|
{ timeout: 2000 }
|
|
546
613
|
);
|
|
547
614
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
615
|
+
// Force rerender to pick up ref update (refs don't trigger re-renders)
|
|
616
|
+
rerender();
|
|
617
|
+
|
|
618
|
+
// Wait for stable scope ref to update (happens in useEffect after state update)
|
|
619
|
+
await waitFor(
|
|
620
|
+
() => {
|
|
621
|
+
// When event is provided but validation fails, hook returns scope with just eventId
|
|
622
|
+
// appId must be resolved for scope to be valid
|
|
623
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
624
|
+
},
|
|
625
|
+
{ timeout: 2000, interval: 10 }
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// When event is provided but org derivation fails, hook returns scope with just eventId
|
|
629
|
+
// (no error - org will be derived during permission checks)
|
|
630
|
+
// appId must be present for scope to be valid
|
|
631
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
632
|
+
if (result.current.resolvedScope) {
|
|
633
|
+
expect(result.current.resolvedScope.eventId).toBe('event-123');
|
|
634
|
+
expect(result.current.resolvedScope.appId).toBe('app-123');
|
|
635
|
+
expect(result.current.error).toBeNull();
|
|
636
|
+
}
|
|
552
637
|
});
|
|
553
638
|
|
|
554
639
|
it('handles database error when resolving app ID', async () => {
|