@jmruthers/pace-core 0.5.110 → 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.
Files changed (230) hide show
  1. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  2. package/dist/{DataTable-D3BK2FCN.js → DataTable-5W2HVLLV.js} +8 -8
  3. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
  4. package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
  5. package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
  6. package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
  7. package/dist/chunk-2BIDKXQU.js.map +1 -0
  8. package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
  9. package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
  10. package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
  11. package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
  12. package/dist/chunk-IWJYNWXN.js.map +1 -0
  13. package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
  14. package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
  15. package/dist/chunk-MW73E7SP.js.map +1 -0
  16. package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
  17. package/dist/chunk-PXXS26G5.js.map +1 -0
  18. package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
  19. package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
  20. package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
  21. package/dist/chunk-UGVU7L7N.js.map +1 -0
  22. package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
  23. package/dist/chunk-X7SPKHYZ.js.map +1 -0
  24. package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
  25. package/dist/chunk-ZL45MG76.js.map +1 -0
  26. package/dist/components.js +10 -10
  27. package/dist/hooks.d.ts +11 -1
  28. package/dist/hooks.js +9 -7
  29. package/dist/hooks.js.map +1 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +13 -13
  32. package/dist/providers.d.ts +2 -2
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +13 -8
  35. package/dist/rbac/index.js +9 -9
  36. package/dist/utils.js +1 -1
  37. package/docs/api/classes/ColumnFactory.md +1 -1
  38. package/docs/api/classes/ErrorBoundary.md +1 -1
  39. package/docs/api/classes/InvalidScopeError.md +4 -4
  40. package/docs/api/classes/MissingUserContextError.md +4 -4
  41. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  42. package/docs/api/classes/PermissionDeniedError.md +4 -4
  43. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  44. package/docs/api/classes/RBACAuditManager.md +8 -8
  45. package/docs/api/classes/RBACCache.md +8 -8
  46. package/docs/api/classes/RBACEngine.md +4 -4
  47. package/docs/api/classes/RBACError.md +4 -4
  48. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  49. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  50. package/docs/api/classes/StorageUtils.md +1 -1
  51. package/docs/api/enums/FileCategory.md +1 -1
  52. package/docs/api/interfaces/AggregateConfig.md +1 -1
  53. package/docs/api/interfaces/ButtonProps.md +1 -1
  54. package/docs/api/interfaces/CardProps.md +1 -1
  55. package/docs/api/interfaces/ColorPalette.md +1 -1
  56. package/docs/api/interfaces/ColorShade.md +1 -1
  57. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  58. package/docs/api/interfaces/DataRecord.md +1 -1
  59. package/docs/api/interfaces/DataTableAction.md +1 -1
  60. package/docs/api/interfaces/DataTableColumn.md +1 -1
  61. package/docs/api/interfaces/DataTableProps.md +1 -1
  62. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  63. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  64. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  65. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  66. package/docs/api/interfaces/FileMetadata.md +1 -1
  67. package/docs/api/interfaces/FileReference.md +1 -1
  68. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  69. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  70. package/docs/api/interfaces/FileUploadProps.md +1 -1
  71. package/docs/api/interfaces/FooterProps.md +1 -1
  72. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  73. package/docs/api/interfaces/InputProps.md +1 -1
  74. package/docs/api/interfaces/LabelProps.md +1 -1
  75. package/docs/api/interfaces/LoginFormProps.md +1 -1
  76. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  77. package/docs/api/interfaces/NavigationContextType.md +1 -1
  78. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  79. package/docs/api/interfaces/NavigationItem.md +1 -1
  80. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  81. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  82. package/docs/api/interfaces/Organisation.md +1 -1
  83. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  84. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  85. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  86. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  87. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  88. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  89. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  90. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  91. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  92. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  93. package/docs/api/interfaces/PaletteData.md +1 -1
  94. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  95. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  96. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  98. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  99. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  102. package/docs/api/interfaces/RBACConfig.md +1 -1
  103. package/docs/api/interfaces/RBACLogger.md +1 -1
  104. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  105. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  106. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  107. package/docs/api/interfaces/RouteConfig.md +19 -6
  108. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  109. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  110. package/docs/api/interfaces/StorageConfig.md +1 -1
  111. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  112. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  113. package/docs/api/interfaces/StorageListOptions.md +1 -1
  114. package/docs/api/interfaces/StorageListResult.md +1 -1
  115. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  116. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  117. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  118. package/docs/api/interfaces/StyleImport.md +1 -1
  119. package/docs/api/interfaces/SwitchProps.md +1 -1
  120. package/docs/api/interfaces/ToastActionElement.md +1 -1
  121. package/docs/api/interfaces/ToastProps.md +1 -1
  122. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  128. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  130. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  133. package/docs/api/interfaces/UserEventAccess.md +1 -1
  134. package/docs/api/interfaces/UserMenuProps.md +1 -1
  135. package/docs/api/interfaces/UserProfile.md +1 -1
  136. package/docs/api/modules.md +36 -36
  137. package/docs/api-reference/hooks.md +8 -4
  138. package/docs/architecture/rpc-function-standards.md +3 -1
  139. package/docs/best-practices/common-patterns.md +3 -3
  140. package/docs/best-practices/deployment.md +10 -4
  141. package/docs/best-practices/performance.md +11 -3
  142. package/docs/core-concepts/organisations.md +8 -8
  143. package/docs/core-concepts/permissions.md +133 -72
  144. package/docs/migration/rbac-migration.md +65 -66
  145. package/docs/rbac/advanced-patterns.md +15 -22
  146. package/docs/rbac/examples.md +12 -12
  147. package/docs/rbac/getting-started.md +3 -3
  148. package/docs/rbac/troubleshooting.md +2 -1
  149. package/package.json +1 -1
  150. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  151. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  152. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  153. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  154. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  155. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  156. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  157. package/src/components/FileUpload/FileUpload.tsx +2 -8
  158. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  159. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  160. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  161. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  162. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  163. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  164. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  165. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  166. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  167. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  168. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  169. package/src/hooks/index.ts +1 -1
  170. package/src/hooks/useFileDisplay.ts +51 -0
  171. package/src/hooks/usePermissionCache.test.ts +112 -68
  172. package/src/hooks/usePermissionCache.ts +55 -15
  173. package/src/rbac/README.md +81 -39
  174. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  175. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  176. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  177. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  178. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  179. package/src/rbac/adapters.tsx +4 -4
  180. package/src/rbac/api.test.ts +37 -13
  181. package/src/rbac/api.ts +25 -8
  182. package/src/rbac/audit.test.ts +2 -2
  183. package/src/rbac/audit.ts +14 -5
  184. package/src/rbac/cache.test.ts +12 -0
  185. package/src/rbac/cache.ts +29 -9
  186. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  187. package/src/rbac/components/NavigationGuard.tsx +14 -14
  188. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  189. package/src/rbac/components/PagePermissionGuard.tsx +4 -3
  190. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  191. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  192. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  193. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  194. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  195. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  196. package/src/rbac/docs/event-based-apps.md +6 -6
  197. package/src/rbac/engine.ts +12 -2
  198. package/src/rbac/hooks/useCan.test.ts +29 -2
  199. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  200. package/src/rbac/hooks/usePermissions.ts +47 -23
  201. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  202. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  203. package/src/rbac/hooks/useRBAC.ts +0 -55
  204. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  205. package/src/rbac/permissions.test.ts +11 -7
  206. package/src/rbac/security.test.ts +2 -2
  207. package/src/rbac/security.ts +22 -7
  208. package/src/rbac/types.test.ts +2 -2
  209. package/src/rbac/types.ts +1 -2
  210. package/src/services/EventService.ts +41 -13
  211. package/src/services/__tests__/EventService.test.ts +25 -4
  212. package/src/services/interfaces/IEventService.ts +1 -0
  213. package/src/utils/file-reference.ts +9 -0
  214. package/dist/chunk-2W4WKJVF.js.map +0 -1
  215. package/dist/chunk-3J5N2T2N.js.map +0 -1
  216. package/dist/chunk-7GBEBJLR.js.map +0 -1
  217. package/dist/chunk-AUXS7XSO.js.map +0 -1
  218. package/dist/chunk-HADXAZT3.js.map +0 -1
  219. package/dist/chunk-Q7APDV6H.js.map +0 -1
  220. package/dist/chunk-XRSP3H52.js.map +0 -1
  221. /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-5W2HVLLV.js.map} +0 -0
  222. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
  223. /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
  224. /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
  225. /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
  226. /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
  227. /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
  228. /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
  229. /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
  230. /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 RBAC hook
16
- vi.mock('../rbac/hooks/useRBAC', () => ({
17
- useRBAC: vi.fn()
14
+ // Mock the dependencies
15
+ vi.mock('../providers/UnifiedAuthProvider', () => ({
16
+ useUnifiedAuth: vi.fn()
18
17
  }));
19
18
 
20
- describe('usePermissionCache', () => {
21
- const mockUseRBAC = vi.mocked(useRBAC);
22
-
23
- // Create stable mock objects to prevent unnecessary re-renders
24
- const stableMockRBAC = {
25
- hasPermission: vi.fn().mockResolvedValue(true),
26
- isSuperAdmin: vi.fn().mockResolvedValue(false),
27
- isOrgAdmin: vi.fn().mockResolvedValue(false),
28
- isEventAdmin: vi.fn().mockResolvedValue(false),
29
- // Add other required properties
30
- } as any;
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
- // Reset the mock functions to return the expected values
35
- stableMockRBAC.hasPermission.mockResolvedValue(true);
36
- stableMockRBAC.isSuperAdmin.mockResolvedValue(false);
37
- stableMockRBAC.isOrgAdmin.mockResolvedValue(false);
38
- stableMockRBAC.isEventAdmin.mockResolvedValue(false);
39
- mockUseRBAC.mockReturnValue(stableMockRBAC);
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 useRBAC hook', () => {
91
+ it('depends on auth and organisation hooks', () => {
69
92
  renderHook(() => usePermissionCache());
70
- expect(mockUseRBAC).toHaveBeenCalled();
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(mockUseRBAC().hasPermission).toHaveBeenCalledWith('read', 'users');
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
- mockUseRBAC.mockReturnValue({
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 RBAC hook gracefully', () => {
118
- mockUseRBAC.mockReturnValue(null as any);
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(mockUseRBAC().hasPermission).toHaveBeenCalledTimes(1);
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(mockUseRBAC().hasPermission).toHaveBeenCalledTimes(2);
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(mockUseRBAC().hasPermission).toHaveBeenCalledTimes(2);
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
- mockUseRBAC.mockReturnValue({
223
- hasPermission: vi.fn().mockImplementation(() =>
224
- new Promise(resolve => setTimeout(() => resolve(true), 100))
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 RBAC hook errors gracefully', async () => {
317
- mockUseRBAC.mockReturnValue({
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(mockUseRBAC().hasPermission).toHaveBeenCalledTimes(2);
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 useRBAC hook correctly', () => {
396
+ describe('Integration with RBAC API', () => {
397
+ it('integrates with auth and organisation hooks correctly', () => {
379
398
  renderHook(() => usePermissionCache());
380
- expect(mockUseRBAC).toHaveBeenCalled();
399
+ expect(mockUseUnifiedAuth).toHaveBeenCalled();
400
+ expect(mockUseOrganisations).toHaveBeenCalled();
381
401
  });
382
402
 
383
- it('passes correct parameters to RBAC hook', async () => {
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(mockUseRBAC().hasPermission).toHaveBeenCalledWith('read', 'users');
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 RBAC hook state changes', async () => {
392
- const mockHasPermission = vi.fn().mockResolvedValue(true);
393
- mockUseRBAC.mockReturnValue({
394
- hasPermission: mockHasPermission,
395
- isSuperAdmin: vi.fn().mockResolvedValue(false),
396
- isOrgAdmin: vi.fn().mockResolvedValue(false),
397
- isEventAdmin: vi.fn().mockResolvedValue(false),
398
- // Add other required properties
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(mockHasPermission).toHaveBeenCalledWith('read', 'users');
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
- * - useRBAC hook - Base permission checking
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 { useRBAC } from '../rbac/hooks/useRBAC';
56
- import type { Operation } from '../rbac/types';
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 base RBAC hook
128
- const rbacHook = useRBAC();
129
- const { hasPermission: baseHasPermission, user } = rbacHook || {};
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 (!baseHasPermission) {
208
- console.warn('[PermissionCache] RBAC not available - permission check failed');
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
- const result = await baseHasPermission(operation, pageId);
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
- }, [baseHasPermission, getCacheKey, isCacheValid, logPermissionCheck, mergedConfig.defaultTTL, mergedConfig.maxCacheSize, triggerCleanup]);
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 (!baseHasPermission) {
284
- console.warn('[PermissionCache] RBAC not available - permission checks failed');
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
- const result = await baseHasPermission(operation, pageId);
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
- }, [baseHasPermission, getCacheKey, isCacheValid, logPermissionCheck, mergedConfig.defaultTTL, mergedConfig.maxCacheSize, triggerCleanup]);
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 }> => {