@jmruthers/pace-core 0.5.109 → 0.5.111
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 +22 -0
- package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-5HITILXS.js → DataTable-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-5I3E47G2.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-P72NKAT5.js → chunk-2BIDKXQU.js} +157 -120
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-S4D3Z723.js → chunk-ACYQNYHB.js} +7 -7
- package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
- package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
- package/dist/chunk-IWJYNWXN.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
- package/dist/chunk-MW73E7SP.js.map +1 -0
- package/dist/{chunk-F6TSYCKP.js → chunk-PXXS26G5.js} +68 -29
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-UW2DE6JX.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-WWNOVFDC.js → chunk-UGVU7L7N.js} +52 -90
- package/dist/chunk-UGVU7L7N.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
- package/dist/chunk-X7SPKHYZ.js.map +1 -0
- package/dist/{chunk-3TKTL5AZ.js → chunk-ZL45MG76.js} +60 -60
- package/dist/chunk-ZL45MG76.js.map +1 -0
- package/dist/components.js +10 -10
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +9 -7
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +46 -29
- package/dist/rbac/index.js +9 -9
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +8 -8
- package/docs/api/classes/RBACEngine.md +9 -8
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +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/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +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 +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +19 -8
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +19 -6
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.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/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 +44 -43
- package/docs/api-reference/hooks.md +8 -4
- package/docs/architecture/rpc-function-standards.md +3 -1
- package/docs/best-practices/common-patterns.md +3 -3
- package/docs/best-practices/deployment.md +10 -4
- package/docs/best-practices/performance.md +11 -3
- package/docs/core-concepts/organisations.md +8 -8
- package/docs/core-concepts/permissions.md +133 -72
- package/docs/documentation-index.md +0 -2
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/README.md +114 -38
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/api-reference.md +63 -16
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +19 -19
- package/docs/rbac/quick-start.md +110 -35
- package/docs/rbac/troubleshooting.md +127 -3
- package/package.json +1 -1
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
- package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
- package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
- package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useFileDisplay.ts +51 -0
- package/src/hooks/usePermissionCache.test.ts +112 -68
- package/src/hooks/usePermissionCache.ts +55 -15
- package/src/rbac/README.md +81 -39
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
- package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
- package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
- package/src/rbac/adapters.tsx +4 -4
- package/src/rbac/api.test.ts +39 -15
- package/src/rbac/api.ts +27 -9
- package/src/rbac/audit.test.ts +2 -2
- package/src/rbac/audit.ts +14 -5
- package/src/rbac/cache.test.ts +12 -0
- package/src/rbac/cache.ts +29 -9
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +14 -14
- package/src/rbac/components/NavigationProvider.test.tsx +1 -1
- package/src/rbac/components/PagePermissionGuard.tsx +22 -38
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +19 -15
- package/src/rbac/components/RoleBasedRouter.tsx +16 -9
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +2 -2
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/config.ts +2 -0
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +27 -7
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +47 -23
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
- package/src/rbac/hooks/useRBAC.test.ts +3 -40
- package/src/rbac/hooks/useRBAC.ts +0 -55
- package/src/rbac/hooks/useResolvedScope.ts +23 -31
- package/src/rbac/permissions.test.ts +11 -7
- package/src/rbac/security.test.ts +2 -2
- package/src/rbac/security.ts +23 -8
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +41 -13
- package/src/services/__tests__/EventService.test.ts +25 -4
- package/src/services/interfaces/IEventService.ts +1 -0
- package/src/utils/file-reference.ts +9 -0
- package/dist/chunk-2W4WKJVF.js.map +0 -1
- package/dist/chunk-3TKTL5AZ.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-F6TSYCKP.js.map +0 -1
- package/dist/chunk-P72NKAT5.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-WWNOVFDC.js.map +0 -1
- package/docs/rbac/breaking-changes-v3.md +0 -222
- package/docs/rbac/migration-guide.md +0 -260
- /package/dist/{DataTable-5HITILXS.js.map → DataTable-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-5I3E47G2.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-S4D3Z723.js.map → chunk-ACYQNYHB.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
- /package/dist/{chunk-UW2DE6JX.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
|
@@ -156,6 +156,33 @@ export function useFileDisplay(
|
|
|
156
156
|
const cached = authenticatedFileCache.get(cacheKey);
|
|
157
157
|
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
158
158
|
const cachedData = cached.data;
|
|
159
|
+
|
|
160
|
+
// FIX: Regenerate signed URL for private files if missing
|
|
161
|
+
if (cachedData.fileReference && !cachedData.fileUrl &&
|
|
162
|
+
cachedData.fileReference.is_public === false && supabase) {
|
|
163
|
+
// Regenerate signed URL without fetching from database
|
|
164
|
+
try {
|
|
165
|
+
const signedUrlResult = await getSignedUrl(supabase, cachedData.fileReference.file_path, {
|
|
166
|
+
appName: 'pace-core',
|
|
167
|
+
orgId: organisation_id,
|
|
168
|
+
expiresIn: 3600
|
|
169
|
+
});
|
|
170
|
+
const regeneratedUrl = signedUrlResult?.url || null;
|
|
171
|
+
setFileUrl(regeneratedUrl);
|
|
172
|
+
setFileReference(cachedData.fileReference);
|
|
173
|
+
setFileReferences(cachedData.fileReferences || []);
|
|
174
|
+
setFileUrls(cachedData.fileUrls || new Map());
|
|
175
|
+
setFileCount(cachedData.fileCount || 0);
|
|
176
|
+
setIsLoading(false);
|
|
177
|
+
setError(null);
|
|
178
|
+
return;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// If signed URL regeneration fails, fall through to normal fetch
|
|
181
|
+
console.warn('[useFileDisplay] Failed to regenerate signed URL from cache, falling back to fetch:', err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Normal cache hit for public files or files with URLs
|
|
159
186
|
setFileUrl(cachedData.fileUrl || null);
|
|
160
187
|
setFileReference(cachedData.fileReference || null);
|
|
161
188
|
setFileReferences(cachedData.fileReferences || []);
|
|
@@ -387,3 +414,27 @@ export function getFileDisplayCacheStats(): { size: number; keys: string[] } {
|
|
|
387
414
|
};
|
|
388
415
|
}
|
|
389
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Invalidate cache for a specific file display entry
|
|
419
|
+
* Useful for clearing cache after file uploads or updates
|
|
420
|
+
*
|
|
421
|
+
* @param table_name - The table name containing the file reference
|
|
422
|
+
* @param record_id - The record ID that owns the file(s)
|
|
423
|
+
* @param organisation_id - The organisation ID for storage path
|
|
424
|
+
* @param category - Optional file category to invalidate (if provided, also invalidates 'all' category)
|
|
425
|
+
*/
|
|
426
|
+
export function invalidateFileDisplayCache(
|
|
427
|
+
table_name: string,
|
|
428
|
+
record_id: string,
|
|
429
|
+
organisation_id: string,
|
|
430
|
+
category?: FileCategory
|
|
431
|
+
): void {
|
|
432
|
+
const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
|
|
433
|
+
authenticatedFileCache.delete(cacheKey);
|
|
434
|
+
// Also invalidate 'all' category if specific category invalidated
|
|
435
|
+
if (category) {
|
|
436
|
+
const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id}_all`;
|
|
437
|
+
authenticatedFileCache.delete(allCategoryKey);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
@@ -10,33 +10,56 @@
|
|
|
10
10
|
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
11
11
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
12
|
import { usePermissionCache } from './usePermissionCache';
|
|
13
|
-
import { useRBAC } from '../rbac/hooks/useRBAC';
|
|
14
13
|
|
|
15
|
-
// Mock the
|
|
16
|
-
vi.mock('../
|
|
17
|
-
|
|
14
|
+
// Mock the dependencies
|
|
15
|
+
vi.mock('../providers/UnifiedAuthProvider', () => ({
|
|
16
|
+
useUnifiedAuth: vi.fn()
|
|
18
17
|
}));
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
vi.mock('./useOrganisations', () => ({
|
|
20
|
+
useOrganisations: vi.fn()
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('./useEvents', () => ({
|
|
24
|
+
useEvents: vi.fn()
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('../rbac/api', () => ({
|
|
28
|
+
isPermittedCached: vi.fn()
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
|
|
32
|
+
import { useOrganisations } from './useOrganisations';
|
|
33
|
+
import { useEvents } from './useEvents';
|
|
34
|
+
import { isPermittedCached } from '../rbac/api';
|
|
35
|
+
|
|
36
|
+
const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
|
|
37
|
+
const mockUseOrganisations = vi.mocked(useOrganisations);
|
|
38
|
+
const mockUseEvents = vi.mocked(useEvents);
|
|
39
|
+
const mockIsPermittedCached = vi.mocked(isPermittedCached);
|
|
31
40
|
|
|
41
|
+
describe('usePermissionCache', () => {
|
|
32
42
|
beforeEach(() => {
|
|
33
43
|
vi.clearAllMocks();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
|
|
45
|
+
// Setup default mocks
|
|
46
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
47
|
+
user: { id: 'user-123' },
|
|
48
|
+
session: null,
|
|
49
|
+
appName: 'test-app',
|
|
50
|
+
selectedOrganisationId: 'org-123',
|
|
51
|
+
selectedEventId: undefined
|
|
52
|
+
} as any);
|
|
53
|
+
|
|
54
|
+
mockUseOrganisations.mockReturnValue({
|
|
55
|
+
selectedOrganisation: { id: 'org-123' }
|
|
56
|
+
} as any);
|
|
57
|
+
|
|
58
|
+
mockUseEvents.mockReturnValue({
|
|
59
|
+
selectedEvent: null
|
|
60
|
+
} as any);
|
|
61
|
+
|
|
62
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
40
63
|
});
|
|
41
64
|
|
|
42
65
|
describe('Hook Initialization', () => {
|
|
@@ -65,9 +88,10 @@ describe('usePermissionCache', () => {
|
|
|
65
88
|
expect(result.current.getDebugInfo).toBeDefined();
|
|
66
89
|
});
|
|
67
90
|
|
|
68
|
-
it('depends on
|
|
91
|
+
it('depends on auth and organisation hooks', () => {
|
|
69
92
|
renderHook(() => usePermissionCache());
|
|
70
|
-
expect(
|
|
93
|
+
expect(mockUseUnifiedAuth).toHaveBeenCalled();
|
|
94
|
+
expect(mockUseOrganisations).toHaveBeenCalled();
|
|
71
95
|
});
|
|
72
96
|
});
|
|
73
97
|
|
|
@@ -77,7 +101,14 @@ describe('usePermissionCache', () => {
|
|
|
77
101
|
|
|
78
102
|
const hasPermission = await result.current.checkPermission('read', 'users');
|
|
79
103
|
expect(hasPermission).toBe(true);
|
|
80
|
-
expect(
|
|
104
|
+
expect(mockIsPermittedCached).toHaveBeenCalledWith(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
userId: 'user-123',
|
|
107
|
+
scope: expect.objectContaining({ organisationId: 'org-123' }),
|
|
108
|
+
permission: 'read:page.users',
|
|
109
|
+
pageId: 'users'
|
|
110
|
+
})
|
|
111
|
+
);
|
|
81
112
|
|
|
82
113
|
// Check that result is cached
|
|
83
114
|
const cachedResult = await result.current.checkPermission('read', 'users');
|
|
@@ -100,13 +131,7 @@ describe('usePermissionCache', () => {
|
|
|
100
131
|
});
|
|
101
132
|
|
|
102
133
|
it('handles permission check errors gracefully', async () => {
|
|
103
|
-
|
|
104
|
-
hasPermission: vi.fn().mockRejectedValue(new Error('Permission check failed')),
|
|
105
|
-
isSuperAdmin: vi.fn().mockResolvedValue(false),
|
|
106
|
-
isOrgAdmin: vi.fn().mockResolvedValue(false),
|
|
107
|
-
isEventAdmin: vi.fn().mockResolvedValue(false),
|
|
108
|
-
// Add other required properties
|
|
109
|
-
} as any);
|
|
134
|
+
mockIsPermittedCached.mockRejectedValueOnce(new Error('Permission check failed'));
|
|
110
135
|
|
|
111
136
|
const { result } = renderHook(() => usePermissionCache());
|
|
112
137
|
|
|
@@ -114,8 +139,14 @@ describe('usePermissionCache', () => {
|
|
|
114
139
|
expect(hasPermission).toBe(false);
|
|
115
140
|
});
|
|
116
141
|
|
|
117
|
-
it('handles missing
|
|
118
|
-
|
|
142
|
+
it('handles missing user context gracefully', () => {
|
|
143
|
+
mockUseUnifiedAuth.mockReturnValueOnce({
|
|
144
|
+
user: null,
|
|
145
|
+
session: null,
|
|
146
|
+
appName: 'test-app',
|
|
147
|
+
selectedOrganisationId: undefined,
|
|
148
|
+
selectedEventId: undefined
|
|
149
|
+
} as any);
|
|
119
150
|
|
|
120
151
|
const { result } = renderHook(() => usePermissionCache());
|
|
121
152
|
|
|
@@ -134,7 +165,7 @@ describe('usePermissionCache', () => {
|
|
|
134
165
|
// Second check should use cache
|
|
135
166
|
await result.current.checkPermission('read', 'users');
|
|
136
167
|
|
|
137
|
-
expect(
|
|
168
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(1);
|
|
138
169
|
});
|
|
139
170
|
|
|
140
171
|
it('expires cached results after TTL', async () => {
|
|
@@ -151,7 +182,7 @@ describe('usePermissionCache', () => {
|
|
|
151
182
|
// Second check should not use cache
|
|
152
183
|
await result.current.checkPermission('read', 'users');
|
|
153
184
|
|
|
154
|
-
expect(
|
|
185
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(2);
|
|
155
186
|
});
|
|
156
187
|
|
|
157
188
|
it('invalidates cache correctly', async () => {
|
|
@@ -166,7 +197,7 @@ describe('usePermissionCache', () => {
|
|
|
166
197
|
// Check again - should not use cache
|
|
167
198
|
await result.current.checkPermission('read', 'users');
|
|
168
199
|
|
|
169
|
-
expect(
|
|
200
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(2);
|
|
170
201
|
});
|
|
171
202
|
|
|
172
203
|
it('respects max cache size', async () => {
|
|
@@ -219,15 +250,9 @@ describe('usePermissionCache', () => {
|
|
|
219
250
|
const { result } = renderHook(() => usePermissionCache());
|
|
220
251
|
|
|
221
252
|
// Mock slow permission check
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
),
|
|
226
|
-
isSuperAdmin: vi.fn().mockResolvedValue(false),
|
|
227
|
-
isOrgAdmin: vi.fn().mockResolvedValue(false),
|
|
228
|
-
isEventAdmin: vi.fn().mockResolvedValue(false),
|
|
229
|
-
// Add other required properties
|
|
230
|
-
} as any);
|
|
253
|
+
mockIsPermittedCached.mockImplementation(() =>
|
|
254
|
+
new Promise(resolve => setTimeout(() => resolve(true), 100))
|
|
255
|
+
);
|
|
231
256
|
|
|
232
257
|
await result.current.checkPermission('read', 'users');
|
|
233
258
|
|
|
@@ -313,14 +338,8 @@ describe('usePermissionCache', () => {
|
|
|
313
338
|
});
|
|
314
339
|
|
|
315
340
|
describe('Error Handling', () => {
|
|
316
|
-
it('handles
|
|
317
|
-
|
|
318
|
-
hasPermission: vi.fn().mockRejectedValue(new Error('RBAC error')),
|
|
319
|
-
isSuperAdmin: vi.fn().mockResolvedValue(false),
|
|
320
|
-
isOrgAdmin: vi.fn().mockResolvedValue(false),
|
|
321
|
-
isEventAdmin: vi.fn().mockResolvedValue(false),
|
|
322
|
-
// Add other required properties
|
|
323
|
-
} as any);
|
|
341
|
+
it('handles permission API errors gracefully', async () => {
|
|
342
|
+
mockIsPermittedCached.mockRejectedValueOnce(new Error('RBAC error'));
|
|
324
343
|
|
|
325
344
|
const { result } = renderHook(() => usePermissionCache());
|
|
326
345
|
|
|
@@ -357,7 +376,7 @@ describe('usePermissionCache', () => {
|
|
|
357
376
|
// Should not use cache
|
|
358
377
|
await result.current.checkPermission('read', 'users');
|
|
359
378
|
|
|
360
|
-
expect(
|
|
379
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(2);
|
|
361
380
|
});
|
|
362
381
|
|
|
363
382
|
it('respects max cache size configuration', async () => {
|
|
@@ -374,34 +393,59 @@ describe('usePermissionCache', () => {
|
|
|
374
393
|
});
|
|
375
394
|
});
|
|
376
395
|
|
|
377
|
-
describe('Integration with RBAC', () => {
|
|
378
|
-
it('integrates with
|
|
396
|
+
describe('Integration with RBAC API', () => {
|
|
397
|
+
it('integrates with auth and organisation hooks correctly', () => {
|
|
379
398
|
renderHook(() => usePermissionCache());
|
|
380
|
-
expect(
|
|
399
|
+
expect(mockUseUnifiedAuth).toHaveBeenCalled();
|
|
400
|
+
expect(mockUseOrganisations).toHaveBeenCalled();
|
|
381
401
|
});
|
|
382
402
|
|
|
383
|
-
it('passes correct parameters to
|
|
403
|
+
it('passes correct parameters to isPermittedCached', async () => {
|
|
384
404
|
const { result } = renderHook(() => usePermissionCache());
|
|
385
405
|
|
|
386
406
|
await result.current.checkPermission('read', 'users');
|
|
387
407
|
|
|
388
|
-
expect(
|
|
408
|
+
expect(mockIsPermittedCached).toHaveBeenCalledWith(
|
|
409
|
+
expect.objectContaining({
|
|
410
|
+
userId: 'user-123',
|
|
411
|
+
scope: expect.objectContaining({ organisationId: 'org-123' }),
|
|
412
|
+
permission: 'read:page.users',
|
|
413
|
+
pageId: 'users'
|
|
414
|
+
})
|
|
415
|
+
);
|
|
389
416
|
});
|
|
390
417
|
|
|
391
|
-
it('handles
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
418
|
+
it('handles permission API state changes', async () => {
|
|
419
|
+
mockIsPermittedCached.mockResolvedValueOnce(true);
|
|
420
|
+
mockUseUnifiedAuth.mockReturnValueOnce({
|
|
421
|
+
user: { id: 'user-456' },
|
|
422
|
+
session: null,
|
|
423
|
+
appName: 'test-app',
|
|
424
|
+
selectedOrganisationId: undefined,
|
|
425
|
+
selectedEventId: undefined
|
|
426
|
+
} as any);
|
|
427
|
+
|
|
428
|
+
// The hook uses useOrganisations() for organisationId, not useUnifiedAuth
|
|
429
|
+
// The mock for useOrganisations returns 'org-123' by default
|
|
430
|
+
mockUseOrganisations.mockReturnValueOnce({
|
|
431
|
+
selectedOrganisation: { id: 'org-123' },
|
|
432
|
+
organisations: [],
|
|
433
|
+
userMemberships: [],
|
|
434
|
+
isLoading: false,
|
|
435
|
+
error: null
|
|
399
436
|
} as any);
|
|
400
437
|
|
|
401
438
|
const { result } = renderHook(() => usePermissionCache());
|
|
402
439
|
|
|
403
440
|
await result.current.checkPermission('read', 'users');
|
|
404
|
-
expect(
|
|
441
|
+
expect(mockIsPermittedCached).toHaveBeenCalledWith(
|
|
442
|
+
expect.objectContaining({
|
|
443
|
+
userId: 'user-456',
|
|
444
|
+
scope: expect.objectContaining({ organisationId: 'org-123' }),
|
|
445
|
+
permission: 'read:page.users',
|
|
446
|
+
pageId: 'users'
|
|
447
|
+
})
|
|
448
|
+
);
|
|
405
449
|
});
|
|
406
450
|
});
|
|
407
451
|
});
|
|
@@ -47,13 +47,19 @@
|
|
|
47
47
|
*
|
|
48
48
|
* @dependencies
|
|
49
49
|
* - React 18+ - Hooks and effects
|
|
50
|
-
* -
|
|
50
|
+
* - RBAC API functions - isPermittedCached for permission checking
|
|
51
51
|
* - RBAC types - Type definitions
|
|
52
|
+
*
|
|
53
|
+
* @deprecated Consider using useCan() or usePermissions() hooks directly instead.
|
|
54
|
+
* This hook provides additional caching on top of the built-in caching in useCan/usePermissions.
|
|
52
55
|
*/
|
|
53
56
|
|
|
54
57
|
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
55
|
-
import {
|
|
56
|
-
import
|
|
58
|
+
import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
|
|
59
|
+
import { useOrganisations } from './useOrganisations';
|
|
60
|
+
import { useEvents } from './useEvents';
|
|
61
|
+
import { isPermittedCached } from '../rbac/api';
|
|
62
|
+
import type { Operation, Permission, Scope, UUID } from '../rbac/types';
|
|
57
63
|
|
|
58
64
|
// Cache entry interface
|
|
59
65
|
interface CacheEntry {
|
|
@@ -124,9 +130,24 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
124
130
|
userId?: string;
|
|
125
131
|
}>>([]);
|
|
126
132
|
|
|
127
|
-
// Get
|
|
128
|
-
const
|
|
129
|
-
const {
|
|
133
|
+
// Get auth context for userId and scope
|
|
134
|
+
const { user } = useUnifiedAuth();
|
|
135
|
+
const { selectedOrganisation } = useOrganisations();
|
|
136
|
+
|
|
137
|
+
let selectedEvent: { event_id: string } | null = null;
|
|
138
|
+
try {
|
|
139
|
+
const eventsContext = useEvents();
|
|
140
|
+
selectedEvent = eventsContext.selectedEvent;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// Event provider not available - continue without event context
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Build scope for permission checks
|
|
146
|
+
const scope: Scope = useMemo(() => ({
|
|
147
|
+
organisationId: selectedOrganisation?.id || '',
|
|
148
|
+
eventId: selectedEvent?.event_id || undefined,
|
|
149
|
+
appId: undefined
|
|
150
|
+
}), [selectedOrganisation?.id, selectedEvent?.event_id]);
|
|
130
151
|
|
|
131
152
|
// Generate cache key
|
|
132
153
|
const getCacheKey = useCallback((operation: Operation, pageId: string): string => {
|
|
@@ -204,8 +225,8 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
204
225
|
return false;
|
|
205
226
|
}
|
|
206
227
|
|
|
207
|
-
if (!
|
|
208
|
-
console.warn('[PermissionCache]
|
|
228
|
+
if (!user?.id || !scope.organisationId) {
|
|
229
|
+
console.warn('[PermissionCache] User or organisation context not available - permission check failed');
|
|
209
230
|
return false;
|
|
210
231
|
}
|
|
211
232
|
|
|
@@ -229,7 +250,17 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
229
250
|
// Create new promise for this permission check
|
|
230
251
|
const permissionPromise = (async () => {
|
|
231
252
|
try {
|
|
232
|
-
|
|
253
|
+
// Build permission string: operation:page.pageId
|
|
254
|
+
const permission: Permission = pageId
|
|
255
|
+
? `${operation}:page.${pageId}` as Permission
|
|
256
|
+
: `${operation}:${pageId || ''}` as Permission;
|
|
257
|
+
|
|
258
|
+
const result = await isPermittedCached({
|
|
259
|
+
userId: user.id as UUID,
|
|
260
|
+
scope,
|
|
261
|
+
permission,
|
|
262
|
+
pageId: pageId as UUID | undefined
|
|
263
|
+
});
|
|
233
264
|
const responseTime = Date.now() - startTime;
|
|
234
265
|
|
|
235
266
|
// Cache the result
|
|
@@ -267,7 +298,7 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
267
298
|
promiseCache.current.set(cacheKey, permissionPromise);
|
|
268
299
|
|
|
269
300
|
return permissionPromise;
|
|
270
|
-
}, [
|
|
301
|
+
}, [user?.id, scope, getCacheKey, isCacheValid, logPermissionCheck, mergedConfig.defaultTTL, mergedConfig.maxCacheSize, triggerCleanup]);
|
|
271
302
|
|
|
272
303
|
// Check multiple permissions efficiently
|
|
273
304
|
const checkMultiplePermissions = useCallback(async (
|
|
@@ -280,14 +311,13 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
280
311
|
return [];
|
|
281
312
|
}
|
|
282
313
|
|
|
283
|
-
if (!
|
|
284
|
-
console.warn('[PermissionCache]
|
|
314
|
+
if (!user?.id || !scope.organisationId) {
|
|
315
|
+
console.warn('[PermissionCache] User or organisation context not available - permission checks failed');
|
|
285
316
|
return permissions.map(([operation, pageId]) => ({
|
|
286
317
|
operation,
|
|
287
318
|
pageId,
|
|
288
319
|
hasPermission: false,
|
|
289
320
|
cached: false,
|
|
290
|
-
responseTime: 0,
|
|
291
321
|
timestamp: Date.now()
|
|
292
322
|
}));
|
|
293
323
|
}
|
|
@@ -324,7 +354,17 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
324
354
|
// For now, check them individually (could be optimized with batch RPC)
|
|
325
355
|
for (const [operation, pageId, originalIndex] of uncachedPermissions) {
|
|
326
356
|
try {
|
|
327
|
-
|
|
357
|
+
// Build permission string: operation:page.pageId
|
|
358
|
+
const permission: Permission = pageId
|
|
359
|
+
? `${operation}:page.${pageId}` as Permission
|
|
360
|
+
: `${operation}:${pageId || ''}` as Permission;
|
|
361
|
+
|
|
362
|
+
const result = await isPermittedCached({
|
|
363
|
+
userId: user.id as UUID,
|
|
364
|
+
scope,
|
|
365
|
+
permission,
|
|
366
|
+
pageId: pageId as UUID | undefined
|
|
367
|
+
});
|
|
328
368
|
const cacheKey = getCacheKey(operation, pageId);
|
|
329
369
|
|
|
330
370
|
// Cache the result
|
|
@@ -376,7 +416,7 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
376
416
|
stats.current.totalResponseTime += Date.now() - startTime;
|
|
377
417
|
|
|
378
418
|
return results;
|
|
379
|
-
}, [
|
|
419
|
+
}, [user?.id, scope, getCacheKey, isCacheValid, logPermissionCheck, mergedConfig.defaultTTL, mergedConfig.maxCacheSize, triggerCleanup]);
|
|
380
420
|
|
|
381
421
|
// Get cached permissions for a page
|
|
382
422
|
const getCachedPermissions = useCallback((pageId: string): Array<{ operation: Operation; hasPermission: boolean }> => {
|