@jmruthers/pace-core 0.5.186 → 0.5.188
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/dist/{DataTable-IX2NBUTP.js → DataTable-GUFUNZ3N.js} +7 -7
- package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-643PUAIM.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-HGPQUCBC.js → chunk-2UUZZJFT.js} +3 -3
- package/dist/{chunk-445GEP27.js → chunk-3GOZZZYH.js} +33 -8
- package/dist/chunk-3GOZZZYH.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-DAGICKHT.js → chunk-DDM4CCYT.js} +3 -3
- package/dist/{chunk-XAUHJD3L.js → chunk-E7UAOUMY.js} +2 -2
- package/dist/{chunk-HDCUMOOI.js → chunk-EFCLXK7F.js} +792 -559
- package/dist/chunk-EFCLXK7F.js.map +1 -0
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-GRIQLQ52.js → chunk-IM4QE42D.js} +27 -23
- package/dist/chunk-IM4QE42D.js.map +1 -0
- package/dist/{chunk-OALXJH4Y.js → chunk-IPCH26AG.js} +8 -8
- package/dist/chunk-IPCH26AG.js.map +1 -0
- package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
- package/dist/{chunk-TC7D3CR3.js → chunk-UNOTYLQF.js} +556 -101
- package/dist/chunk-UNOTYLQF.js.map +1 -0
- package/dist/{chunk-FXFJRTKI.js → chunk-VGZZXKBR.js} +5 -5
- package/dist/chunk-VGZZXKBR.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
- package/dist/hooks.d.ts +221 -6
- package/dist/hooks.js +146 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +24 -9
- package/dist/index.js +62 -28
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +124 -7
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
- package/dist/utils.d.ts +213 -3
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- 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 +1 -1
- package/docs/api/classes/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/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 +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- 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 +15 -15
- 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 +1 -1
- 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 +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- 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 +26 -3
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/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 +318 -59
- package/docs/best-practices/performance.md +11 -0
- package/docs/getting-started/examples/README.md +2 -2
- package/docs/implementation-guides/file-upload-storage.md +29 -0
- package/docs/implementation-guides/public-pages.md +140 -1230
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +14 -0
- package/docs/standards/07-rbac-and-rls-standard.md +356 -0
- package/package.json +1 -1
- package/src/__tests__/public-recipe-view.test.ts +199 -0
- package/src/__tests__/rls-policies.test.ts +333 -0
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +6 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +20 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/services/OrganisationService.ts +5 -4
- package/src/types/file-reference.ts +0 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
- package/src/utils/file-reference/index.ts +44 -15
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/storage/helpers.ts +143 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-GRIQLQ52.js.map +0 -1
- package/dist/chunk-HDCUMOOI.js.map +0 -1
- package/dist/chunk-OALXJH4Y.js.map +0 -1
- package/dist/chunk-TC7D3CR3.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-2UUZZJFT.js.map} +0 -0
- /package/dist/{chunk-DAGICKHT.js.map → chunk-DDM4CCYT.js.map} +0 -0
- /package/dist/{chunk-XAUHJD3L.js.map → chunk-E7UAOUMY.js.map} +0 -0
|
@@ -155,6 +155,7 @@ export function useInactivityTracker({
|
|
|
155
155
|
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
156
156
|
const lastActivityRef = useRef<number>(Date.now());
|
|
157
157
|
const channelRef = useRef<BroadcastChannel | null>(null);
|
|
158
|
+
const throttledResetActivityRef = useRef<((event: Event) => void) | null>(null);
|
|
158
159
|
|
|
159
160
|
// Clear all timers
|
|
160
161
|
const clearTimers = useCallback(() => {
|
|
@@ -294,47 +295,56 @@ export function useInactivityTracker({
|
|
|
294
295
|
logger.warn('useInactivityTracker', 'Failed to check persisted activity time:', error);
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
resetActivity();
|
|
300
|
-
}, 100);
|
|
301
|
-
|
|
302
|
-
// Add event listeners
|
|
303
|
-
const addEventListeners = () => {
|
|
298
|
+
// Clean up any existing throttled handler and event listeners first
|
|
299
|
+
if (throttledResetActivityRef.current) {
|
|
304
300
|
ACTIVITY_EVENTS.forEach(event => {
|
|
305
|
-
document.
|
|
301
|
+
document.removeEventListener(event, throttledResetActivityRef.current!);
|
|
306
302
|
});
|
|
307
|
-
}
|
|
303
|
+
}
|
|
308
304
|
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
});
|
|
314
|
-
};
|
|
305
|
+
// Set up throttled activity handler - store in ref for proper cleanup
|
|
306
|
+
throttledResetActivityRef.current = throttle((event) => {
|
|
307
|
+
resetActivity();
|
|
308
|
+
}, 100);
|
|
315
309
|
|
|
316
|
-
// Add listeners
|
|
317
|
-
|
|
310
|
+
// Add event listeners
|
|
311
|
+
ACTIVITY_EVENTS.forEach(event => {
|
|
312
|
+
document.addEventListener(event, throttledResetActivityRef.current!, { passive: true });
|
|
313
|
+
});
|
|
318
314
|
|
|
319
315
|
// Start the timer (skip activity callback for initial setup)
|
|
320
316
|
resetActivity(true);
|
|
321
317
|
|
|
322
318
|
// Cleanup function
|
|
323
319
|
return () => {
|
|
324
|
-
|
|
320
|
+
// Remove event listeners using the stored ref
|
|
321
|
+
if (throttledResetActivityRef.current) {
|
|
322
|
+
ACTIVITY_EVENTS.forEach(event => {
|
|
323
|
+
document.removeEventListener(event, throttledResetActivityRef.current!);
|
|
324
|
+
});
|
|
325
|
+
throttledResetActivityRef.current = null;
|
|
326
|
+
}
|
|
325
327
|
clearTimers();
|
|
326
328
|
if (channelRef.current) {
|
|
327
329
|
channelRef.current.close();
|
|
328
330
|
channelRef.current = null;
|
|
329
331
|
}
|
|
330
332
|
};
|
|
331
|
-
}, [enabled, isTracking, channelName, storageKey, idleTimeoutMs, warnBeforeMs, onIdle, onWarning]);
|
|
333
|
+
}, [enabled, isTracking, channelName, storageKey, idleTimeoutMs, warnBeforeMs, onIdle, onWarning, onActivity, resetActivity]);
|
|
332
334
|
|
|
333
335
|
// Stop tracking
|
|
334
336
|
const stopTracking = useCallback(() => {
|
|
335
337
|
setIsTracking(false);
|
|
336
338
|
clearTimers();
|
|
337
339
|
|
|
340
|
+
// Remove event listeners
|
|
341
|
+
if (throttledResetActivityRef.current) {
|
|
342
|
+
ACTIVITY_EVENTS.forEach(event => {
|
|
343
|
+
document.removeEventListener(event, throttledResetActivityRef.current!);
|
|
344
|
+
});
|
|
345
|
+
throttledResetActivityRef.current = null;
|
|
346
|
+
}
|
|
347
|
+
|
|
338
348
|
if (channelRef.current) {
|
|
339
349
|
channelRef.current.close();
|
|
340
350
|
channelRef.current = null;
|
|
@@ -348,8 +358,9 @@ export function useInactivityTracker({
|
|
|
348
358
|
return cleanup;
|
|
349
359
|
} else {
|
|
350
360
|
stopTracking();
|
|
361
|
+
return undefined;
|
|
351
362
|
}
|
|
352
|
-
}, [enabled, idleTimeoutMs, warnBeforeMs]);
|
|
363
|
+
}, [enabled, idleTimeoutMs, warnBeforeMs, startTracking, stopTracking]);
|
|
353
364
|
|
|
354
365
|
// Cleanup on unmount
|
|
355
366
|
useEffect(() => {
|
|
@@ -26,6 +26,7 @@ vi.mock('./useOrganisations', () => ({
|
|
|
26
26
|
// Mock the RBAC API
|
|
27
27
|
vi.mock('../rbac/api', () => ({
|
|
28
28
|
isPermitted: vi.fn(),
|
|
29
|
+
isPermittedCached: vi.fn(),
|
|
29
30
|
isSuperAdmin: vi.fn(),
|
|
30
31
|
getPermissionMap: vi.fn()
|
|
31
32
|
}));
|
|
@@ -35,13 +36,14 @@ vi.mock('../rbac/audit', () => ({
|
|
|
35
36
|
emitAuditEvent: vi.fn()
|
|
36
37
|
}));
|
|
37
38
|
|
|
38
|
-
import { isPermitted, isSuperAdmin, getPermissionMap } from '../rbac/api';
|
|
39
|
+
import { isPermitted, isPermittedCached, isSuperAdmin, getPermissionMap } from '../rbac/api';
|
|
39
40
|
import { emitAuditEvent } from '../rbac/audit';
|
|
40
41
|
|
|
41
42
|
describe('useOrganisationSecurity', () => {
|
|
42
43
|
const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
|
|
43
44
|
const mockUseOrganisations = vi.mocked(useOrganisations);
|
|
44
45
|
const mockIsPermitted = vi.mocked(isPermitted);
|
|
46
|
+
const mockIsPermittedCached = vi.mocked(isPermittedCached);
|
|
45
47
|
const mockIsSuperAdmin = vi.mocked(isSuperAdmin);
|
|
46
48
|
const mockGetPermissionMap = vi.mocked(getPermissionMap);
|
|
47
49
|
const mockEmitAuditEvent = vi.mocked(emitAuditEvent);
|
|
@@ -99,6 +101,7 @@ describe('useOrganisationSecurity', () => {
|
|
|
99
101
|
mockUseUnifiedAuth.mockClear();
|
|
100
102
|
mockUseOrganisations.mockClear();
|
|
101
103
|
mockIsPermitted.mockClear();
|
|
104
|
+
mockIsPermittedCached.mockClear();
|
|
102
105
|
mockIsSuperAdmin.mockClear();
|
|
103
106
|
mockGetPermissionMap.mockClear();
|
|
104
107
|
mockEmitAuditEvent.mockClear();
|
|
@@ -508,13 +511,13 @@ describe('useOrganisationSecurity', () => {
|
|
|
508
511
|
signOut: vi.fn(),
|
|
509
512
|
} as any);
|
|
510
513
|
|
|
511
|
-
|
|
514
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
512
515
|
|
|
513
516
|
const { result } = renderHook(() => useOrganisationSecurity());
|
|
514
517
|
|
|
515
518
|
const hasPermission = await result.current.hasPermission('read:users');
|
|
516
519
|
expect(hasPermission).toBe(true);
|
|
517
|
-
expect(
|
|
520
|
+
expect(mockIsPermittedCached).toHaveBeenCalledWith({
|
|
518
521
|
userId: 'user-123',
|
|
519
522
|
scope: {
|
|
520
523
|
organisationId: 'org-123',
|
|
@@ -527,13 +530,13 @@ describe('useOrganisationSecurity', () => {
|
|
|
527
530
|
|
|
528
531
|
it('checks permissions for specific organisation', async () => {
|
|
529
532
|
mockIsSuperAdmin.mockResolvedValue(false);
|
|
530
|
-
|
|
533
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
531
534
|
|
|
532
535
|
const { result } = renderHook(() => useOrganisationSecurity());
|
|
533
536
|
|
|
534
537
|
const hasPermission = await result.current.hasPermission('read:users', 'org-456');
|
|
535
538
|
expect(hasPermission).toBe(true);
|
|
536
|
-
expect(
|
|
539
|
+
expect(mockIsPermittedCached).toHaveBeenCalledWith({
|
|
537
540
|
userId: 'user-123',
|
|
538
541
|
scope: {
|
|
539
542
|
organisationId: 'org-456',
|
|
@@ -546,7 +549,7 @@ describe('useOrganisationSecurity', () => {
|
|
|
546
549
|
|
|
547
550
|
it('handles permission check errors gracefully', async () => {
|
|
548
551
|
mockIsSuperAdmin.mockResolvedValue(false);
|
|
549
|
-
|
|
552
|
+
mockIsPermittedCached.mockRejectedValue(new Error('Permission check failed'));
|
|
550
553
|
|
|
551
554
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
552
555
|
|
|
@@ -622,7 +625,7 @@ describe('useOrganisationSecurity', () => {
|
|
|
622
625
|
|
|
623
626
|
it('handles permission retrieval errors gracefully', async () => {
|
|
624
627
|
mockIsSuperAdmin.mockResolvedValue(false);
|
|
625
|
-
|
|
628
|
+
mockIsPermittedCached.mockRejectedValue(new Error('Permission retrieval failed'));
|
|
626
629
|
|
|
627
630
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
628
631
|
|
|
@@ -163,8 +163,8 @@ export const useOrganisationSecurity = (): OrganisationSecurityHook => {
|
|
|
163
163
|
if (!targetOrgId || !user) return false;
|
|
164
164
|
|
|
165
165
|
try {
|
|
166
|
-
// Use the new RBAC system
|
|
167
|
-
const {
|
|
166
|
+
// Use the new RBAC system with caching
|
|
167
|
+
const { isPermittedCached } = await import('../rbac/api');
|
|
168
168
|
|
|
169
169
|
const scope = {
|
|
170
170
|
organisationId: targetOrgId,
|
|
@@ -172,7 +172,7 @@ export const useOrganisationSecurity = (): OrganisationSecurityHook => {
|
|
|
172
172
|
appId: user.user_metadata?.appId || user.app_metadata?.appId,
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
-
return await
|
|
175
|
+
return await isPermittedCached({
|
|
176
176
|
userId: user.id,
|
|
177
177
|
scope,
|
|
178
178
|
permission: permission as Permission
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Result Caching Hook
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks/QueryCache
|
|
5
|
+
* @since 2.0.0
|
|
6
|
+
*
|
|
7
|
+
* Provides in-memory caching for frequently accessed data to eliminate duplicate queries.
|
|
8
|
+
* Useful for caching user profiles, app pages, and other relatively static data.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useRef, useEffect } from 'react';
|
|
12
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
13
|
+
import { Database } from '../types/database';
|
|
14
|
+
import { createLogger } from '../utils/core/logger';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('useQueryCache');
|
|
17
|
+
|
|
18
|
+
interface CachedQueryEntry<T> {
|
|
19
|
+
data: T;
|
|
20
|
+
expiresAt: number; // Unix timestamp
|
|
21
|
+
promise?: Promise<T>; // For in-flight requests
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* In-memory cache for query results
|
|
26
|
+
* Key format: `table:filterKey:filterValue`
|
|
27
|
+
*/
|
|
28
|
+
const queryCache = new Map<string, CachedQueryEntry<any>>();
|
|
29
|
+
|
|
30
|
+
// Cleanup interval (every 5 minutes)
|
|
31
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
32
|
+
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
33
|
+
|
|
34
|
+
function runCacheCleanup() {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const expiredKeys: string[] = [];
|
|
37
|
+
|
|
38
|
+
queryCache.forEach((entry, key) => {
|
|
39
|
+
if (entry.expiresAt <= now) {
|
|
40
|
+
expiredKeys.push(key);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expiredKeys.forEach(key => {
|
|
45
|
+
queryCache.delete(key);
|
|
46
|
+
log.debug(`Removed expired query from cache: ${key}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialize cleanup timer once
|
|
51
|
+
if (typeof window !== 'undefined' && !cleanupTimer) {
|
|
52
|
+
cleanupTimer = setInterval(runCacheCleanup, CLEANUP_INTERVAL_MS);
|
|
53
|
+
log.debug('Query cache cleanup initialized.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UseQueryCacheOptions {
|
|
57
|
+
/** Time to live in seconds (default: 300 = 5 minutes) */
|
|
58
|
+
ttl?: number;
|
|
59
|
+
/** Whether to enable caching (default: true) */
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface UseQueryCacheReturn {
|
|
64
|
+
/** Get cached query result or fetch if not cached */
|
|
65
|
+
getCachedQuery: <T>(
|
|
66
|
+
table: string,
|
|
67
|
+
filterKey: string,
|
|
68
|
+
filterValue: string,
|
|
69
|
+
fetchFn: () => Promise<T>,
|
|
70
|
+
options?: UseQueryCacheOptions
|
|
71
|
+
) => Promise<T>;
|
|
72
|
+
/** Invalidate a specific cached query */
|
|
73
|
+
invalidateQuery: (table: string, filterKey: string, filterValue: string) => void;
|
|
74
|
+
/** Clear all cached queries */
|
|
75
|
+
clearCache: () => void;
|
|
76
|
+
/** Get cache statistics */
|
|
77
|
+
getCacheStats: () => { size: number; keys: string[] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hook for query result caching
|
|
82
|
+
*
|
|
83
|
+
* Provides caching for frequently accessed data to eliminate duplicate queries.
|
|
84
|
+
* Automatically handles cache expiration and cleanup.
|
|
85
|
+
*
|
|
86
|
+
* @param supabase - Supabase client (optional, can be passed in fetchFn)
|
|
87
|
+
* @returns Query cache utilities
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* const { getCachedQuery } = useQueryCache(supabase);
|
|
92
|
+
*
|
|
93
|
+
* const person = await getCachedQuery(
|
|
94
|
+
* 'pace_person',
|
|
95
|
+
* 'user_id',
|
|
96
|
+
* userId,
|
|
97
|
+
* async () => {
|
|
98
|
+
* const { data } = await supabase
|
|
99
|
+
* .from('pace_person')
|
|
100
|
+
* .select('id, first_name, last_name, email')
|
|
101
|
+
* .eq('user_id', userId)
|
|
102
|
+
* .single();
|
|
103
|
+
* return data;
|
|
104
|
+
* },
|
|
105
|
+
* { ttl: 300 } // 5 minutes
|
|
106
|
+
* );
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function useQueryCache(supabase?: SupabaseClient<Database>): UseQueryCacheReturn {
|
|
110
|
+
const getCachedQuery = useCallback(async <T,>(
|
|
111
|
+
table: string,
|
|
112
|
+
filterKey: string,
|
|
113
|
+
filterValue: string,
|
|
114
|
+
fetchFn: () => Promise<T>,
|
|
115
|
+
options: UseQueryCacheOptions = {}
|
|
116
|
+
): Promise<T> => {
|
|
117
|
+
const { ttl = 300, enabled = true } = options; // Default 5 minutes
|
|
118
|
+
const cacheKey = `${table}:${filterKey}:${filterValue}`;
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
|
|
121
|
+
if (!enabled) {
|
|
122
|
+
return fetchFn();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check cache
|
|
126
|
+
const cached = queryCache.get(cacheKey);
|
|
127
|
+
if (cached) {
|
|
128
|
+
// If data is still valid, return it
|
|
129
|
+
if (cached.expiresAt > now && cached.data !== undefined) {
|
|
130
|
+
log.debug(`Cache hit for query: ${cacheKey}`);
|
|
131
|
+
return cached.data as T;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If there's an in-flight request, wait for it
|
|
135
|
+
if (cached.promise) {
|
|
136
|
+
log.debug(`Waiting for in-flight request: ${cacheKey}`);
|
|
137
|
+
return cached.promise as Promise<T>;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fetch data
|
|
142
|
+
log.debug(`Cache miss for query: ${cacheKey}, fetching...`);
|
|
143
|
+
const fetchPromise = fetchFn();
|
|
144
|
+
|
|
145
|
+
// Store promise for in-flight request deduplication
|
|
146
|
+
queryCache.set(cacheKey, {
|
|
147
|
+
data: undefined as any,
|
|
148
|
+
expiresAt: now + (ttl * 1000),
|
|
149
|
+
promise: fetchPromise,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const data = await fetchPromise;
|
|
154
|
+
|
|
155
|
+
// Update cache with actual data
|
|
156
|
+
queryCache.set(cacheKey, {
|
|
157
|
+
data,
|
|
158
|
+
expiresAt: now + (ttl * 1000),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
log.debug(`Cached query result: ${cacheKey}, expires in ${ttl}s`);
|
|
162
|
+
return data;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Remove failed request from cache
|
|
165
|
+
queryCache.delete(cacheKey);
|
|
166
|
+
log.error(`Query failed for ${cacheKey}:`, error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
const invalidateQuery = useCallback((table: string, filterKey: string, filterValue: string) => {
|
|
172
|
+
const cacheKey = `${table}:${filterKey}:${filterValue}`;
|
|
173
|
+
queryCache.delete(cacheKey);
|
|
174
|
+
log.debug(`Invalidated query cache: ${cacheKey}`);
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
const clearCache = useCallback(() => {
|
|
178
|
+
queryCache.clear();
|
|
179
|
+
log.debug('Cleared all query cache entries.');
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
const getCacheStats = useCallback(() => {
|
|
183
|
+
return {
|
|
184
|
+
size: queryCache.size,
|
|
185
|
+
keys: Array.from(queryCache.keys()),
|
|
186
|
+
};
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
getCachedQuery,
|
|
191
|
+
invalidateQuery,
|
|
192
|
+
clearCache,
|
|
193
|
+
getCacheStats,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Pre-configured cache helpers for common queries
|
|
199
|
+
*/
|
|
200
|
+
export const queryCacheHelpers = {
|
|
201
|
+
/**
|
|
202
|
+
* Cache pace_person queries by user_id
|
|
203
|
+
* TTL: 5 minutes
|
|
204
|
+
*/
|
|
205
|
+
pacePersonByUserId: <T>(
|
|
206
|
+
supabase: SupabaseClient<Database>,
|
|
207
|
+
userId: string,
|
|
208
|
+
fetchFn: () => Promise<T>
|
|
209
|
+
): Promise<T> => {
|
|
210
|
+
const cacheKey = `pace_person:user_id:${userId}`;
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
const ttl = 300 * 1000; // 5 minutes
|
|
213
|
+
|
|
214
|
+
const cached = queryCache.get(cacheKey);
|
|
215
|
+
if (cached && cached.expiresAt > now && cached.data !== undefined) {
|
|
216
|
+
return Promise.resolve(cached.data as T);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (cached?.promise) {
|
|
220
|
+
return cached.promise as Promise<T>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const promise = fetchFn();
|
|
224
|
+
queryCache.set(cacheKey, {
|
|
225
|
+
data: undefined as any,
|
|
226
|
+
expiresAt: now + ttl,
|
|
227
|
+
promise,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
promise.then(data => {
|
|
231
|
+
queryCache.set(cacheKey, { data, expiresAt: now + ttl });
|
|
232
|
+
}).catch(() => {
|
|
233
|
+
queryCache.delete(cacheKey);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return promise;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Cache pace_member queries by person_id
|
|
241
|
+
* TTL: 5 minutes
|
|
242
|
+
*/
|
|
243
|
+
paceMemberByPersonId: <T>(
|
|
244
|
+
supabase: SupabaseClient<Database>,
|
|
245
|
+
personId: string,
|
|
246
|
+
fetchFn: () => Promise<T>
|
|
247
|
+
): Promise<T> => {
|
|
248
|
+
const cacheKey = `pace_member:person_id:${personId}`;
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
const ttl = 300 * 1000; // 5 minutes
|
|
251
|
+
|
|
252
|
+
const cached = queryCache.get(cacheKey);
|
|
253
|
+
if (cached && cached.expiresAt > now && cached.data !== undefined) {
|
|
254
|
+
return Promise.resolve(cached.data as T);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (cached?.promise) {
|
|
258
|
+
return cached.promise as Promise<T>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const promise = fetchFn();
|
|
262
|
+
queryCache.set(cacheKey, {
|
|
263
|
+
data: undefined as any,
|
|
264
|
+
expiresAt: now + ttl,
|
|
265
|
+
promise,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
promise.then(data => {
|
|
269
|
+
queryCache.set(cacheKey, { data, expiresAt: now + ttl });
|
|
270
|
+
}).catch(() => {
|
|
271
|
+
queryCache.delete(cacheKey);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return promise;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Cache rbac_app_pages queries by app_id
|
|
279
|
+
* TTL: 15 minutes (app pages are relatively static)
|
|
280
|
+
*/
|
|
281
|
+
rbacAppPagesByAppId: <T>(
|
|
282
|
+
supabase: SupabaseClient<Database>,
|
|
283
|
+
appId: string,
|
|
284
|
+
fetchFn: () => Promise<T>
|
|
285
|
+
): Promise<T> => {
|
|
286
|
+
const cacheKey = `rbac_app_pages:app_id:${appId}`;
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const ttl = 15 * 60 * 1000; // 15 minutes
|
|
289
|
+
|
|
290
|
+
const cached = queryCache.get(cacheKey);
|
|
291
|
+
if (cached && cached.expiresAt > now && cached.data !== undefined) {
|
|
292
|
+
return Promise.resolve(cached.data as T);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (cached?.promise) {
|
|
296
|
+
return cached.promise as Promise<T>;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const promise = fetchFn();
|
|
300
|
+
queryCache.set(cacheKey, {
|
|
301
|
+
data: undefined as any,
|
|
302
|
+
expiresAt: now + ttl,
|
|
303
|
+
promise,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
promise.then(data => {
|
|
307
|
+
queryCache.set(cacheKey, { data, expiresAt: now + ttl });
|
|
308
|
+
}).catch(() => {
|
|
309
|
+
queryCache.delete(cacheKey);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return promise;
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,8 @@ export type { CardProps } from './components/Card/Card';
|
|
|
76
76
|
|
|
77
77
|
export { Input } from './components/Input/Input';
|
|
78
78
|
export type { InputProps } from './components/Input/Input';
|
|
79
|
+
export { AddressField } from './components/AddressField';
|
|
80
|
+
export type { AddressFieldProps, AddressFieldRef, ParsedAddress, AutocompleteOptions } from './components/AddressField';
|
|
79
81
|
export { Label } from './components/Label/Label';
|
|
80
82
|
export type { LabelProps } from './components/Label/Label';
|
|
81
83
|
|
|
@@ -50,6 +50,7 @@ export function EventServiceProvider({
|
|
|
50
50
|
const eventService = eventServiceRef.current;
|
|
51
51
|
|
|
52
52
|
// Update service dependencies and initialize when dependencies change
|
|
53
|
+
// Note: eventService is a ref and never changes, so we don't include it in dependencies
|
|
53
54
|
useEffect(() => {
|
|
54
55
|
let isMounted = true;
|
|
55
56
|
|
|
@@ -72,7 +73,9 @@ export function EventServiceProvider({
|
|
|
72
73
|
return () => {
|
|
73
74
|
isMounted = false;
|
|
74
75
|
};
|
|
75
|
-
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
// eventService is a ref and never changes, so we exclude it from dependencies
|
|
78
|
+
}, [supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId]);
|
|
76
79
|
|
|
77
80
|
// Cleanup service on unmount only
|
|
78
81
|
useEffect(() => {
|
package/src/rbac/api.test.ts
CHANGED
|
@@ -45,6 +45,12 @@ vi.mock('./cache', () => ({
|
|
|
45
45
|
}
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
+
vi.mock('./request-deduplication', () => ({
|
|
49
|
+
getOrCreateRequest: vi.fn((input, checkFn) => checkFn(input)),
|
|
50
|
+
clearInFlightRequests: vi.fn(),
|
|
51
|
+
getInFlightRequestCount: vi.fn(() => 0),
|
|
52
|
+
}));
|
|
53
|
+
|
|
48
54
|
vi.mock('./config', () => ({
|
|
49
55
|
createRBACConfig: vi.fn(),
|
|
50
56
|
getRBACLogger: vi.fn(() => ({
|
|
@@ -120,7 +126,14 @@ describe('RBAC API', () => {
|
|
|
120
126
|
process.env.NODE_ENV = originalEnv;
|
|
121
127
|
|
|
122
128
|
expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
|
|
123
|
-
expect(mockCreateAuditManager).toHaveBeenCalledWith(
|
|
129
|
+
expect(mockCreateAuditManager).toHaveBeenCalledWith(
|
|
130
|
+
mockSupabase,
|
|
131
|
+
true,
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
batchSize: undefined,
|
|
134
|
+
batchWindow: undefined,
|
|
135
|
+
})
|
|
136
|
+
);
|
|
124
137
|
expect(mockSetGlobalAuditManager).toHaveBeenCalledWith(mockAuditManager);
|
|
125
138
|
expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
|
|
126
139
|
});
|
|
@@ -659,7 +672,7 @@ describe('RBAC API', () => {
|
|
|
659
672
|
});
|
|
660
673
|
|
|
661
674
|
expect(result).toBe(true);
|
|
662
|
-
expect(rbacCache.get).toHaveBeenCalledWith(cacheKey);
|
|
675
|
+
expect(rbacCache.get).toHaveBeenCalledWith(cacheKey, true);
|
|
663
676
|
expect(mockEngine.isPermitted).not.toHaveBeenCalled();
|
|
664
677
|
});
|
|
665
678
|
|
|
@@ -689,10 +702,12 @@ describe('RBAC API', () => {
|
|
|
689
702
|
organisationId: 'org-456'
|
|
690
703
|
})
|
|
691
704
|
);
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
);
|
|
705
|
+
// Check that cache.set was called - pageId presence makes it a page-level check
|
|
706
|
+
expect(rbacCache.set).toHaveBeenCalled();
|
|
707
|
+
const setCall = rbacCache.set.mock.calls[0];
|
|
708
|
+
expect(setCall[0]).toBeTruthy(); // cache key
|
|
709
|
+
expect(setCall[1]).toBe(true); // result
|
|
710
|
+
// setCall[2] is TTL (optional), setCall[3] is useSessionCache (true for page-level)
|
|
696
711
|
});
|
|
697
712
|
});
|
|
698
713
|
|
package/src/rbac/api.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { rbacCache, RBACCache, CACHE_PATTERNS } from './cache';
|
|
|
27
27
|
import { createRBACConfig, RBACConfig, getRBACLogger } from './config';
|
|
28
28
|
import { SecurityContext } from './security';
|
|
29
29
|
import { createLogger } from '../utils/core/logger';
|
|
30
|
+
import { enablePerformanceMonitoring } from './performance';
|
|
31
|
+
import { getOrCreateRequest } from './request-deduplication';
|
|
30
32
|
|
|
31
33
|
const log = createLogger('RBACAPI');
|
|
32
34
|
|
|
@@ -72,10 +74,20 @@ export function setupRBAC(supabase: SupabaseClient<Database>, config?: Partial<R
|
|
|
72
74
|
// Pass security config to engine
|
|
73
75
|
globalEngine = createRBACEngine(supabase, securityConfig);
|
|
74
76
|
|
|
75
|
-
// Setup audit manager
|
|
76
|
-
const
|
|
77
|
+
// Setup audit manager with batching configuration
|
|
78
|
+
const useBatchedAudit = config?.audit?.batched !== false && (config?.performance?.enableBatchedAuditLogging !== false);
|
|
79
|
+
const batchConfig = useBatchedAudit ? {
|
|
80
|
+
batchWindow: config?.audit?.batchWindow,
|
|
81
|
+
batchSize: config?.audit?.batchSize,
|
|
82
|
+
} : undefined;
|
|
83
|
+
const auditManager = createAuditManager(supabase, useBatchedAudit, batchConfig);
|
|
77
84
|
setGlobalAuditManager(auditManager);
|
|
78
85
|
|
|
86
|
+
// Setup performance monitoring if enabled
|
|
87
|
+
if (config?.performance?.enablePerformanceTracking) {
|
|
88
|
+
enablePerformanceMonitoring();
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
logger.info('RBAC system initialized successfully');
|
|
80
92
|
}
|
|
81
93
|
|
|
@@ -195,13 +207,16 @@ export async function isPermitted(input: PermissionCheck): Promise<boolean> {
|
|
|
195
207
|
/**
|
|
196
208
|
* Check if user has a specific permission (cached version)
|
|
197
209
|
*
|
|
210
|
+
* Uses request deduplication to share in-flight requests across components
|
|
211
|
+
* and checks cache before making new requests. Uses session cache for page-level checks.
|
|
212
|
+
*
|
|
198
213
|
* @param input - Permission check input
|
|
199
214
|
* @returns Promise resolving to permission result
|
|
200
215
|
*/
|
|
201
216
|
export async function isPermittedCached(input: PermissionCheck): Promise<boolean> {
|
|
202
217
|
const { userId, scope, permission, pageId } = input;
|
|
203
218
|
|
|
204
|
-
// Check cache first
|
|
219
|
+
// Check cache first (checks both short-term and session cache)
|
|
205
220
|
const cacheKey = RBACCache.generatePermissionKey({
|
|
206
221
|
userId,
|
|
207
222
|
organisationId: scope.organisationId!,
|
|
@@ -211,18 +226,24 @@ export async function isPermittedCached(input: PermissionCheck): Promise<boolean
|
|
|
211
226
|
pageId,
|
|
212
227
|
});
|
|
213
228
|
|
|
214
|
-
const cached = rbacCache.get<boolean>(cacheKey);
|
|
229
|
+
const cached = rbacCache.get<boolean>(cacheKey, true);
|
|
215
230
|
if (cached !== null) {
|
|
216
231
|
return cached;
|
|
217
232
|
}
|
|
218
233
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
// Use request deduplication - if same request is in-flight, share the promise
|
|
235
|
+
return getOrCreateRequest(input, async (checkInput) => {
|
|
236
|
+
// Check permission
|
|
237
|
+
const result = await isPermitted(checkInput);
|
|
238
|
+
|
|
239
|
+
// Determine if this is a page-level check (has pageId or permission contains 'page.')
|
|
240
|
+
const isPageLevelCheck = !!pageId || permission.includes('page.');
|
|
241
|
+
|
|
242
|
+
// Cache result - use session cache for page-level checks
|
|
243
|
+
rbacCache.set(cacheKey, result, undefined, isPageLevelCheck);
|
|
244
|
+
|
|
245
|
+
return result;
|
|
246
|
+
});
|
|
226
247
|
}
|
|
227
248
|
|
|
228
249
|
/**
|