@jmruthers/pace-core 0.5.115 → 0.5.116
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/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-H5KJCAIS.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
- package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- 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/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +35 -12
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- 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/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/EventAppRoleData.md +71 -0
- 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/GrantEventAppRoleParams.md +122 -0
- 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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- 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 +41 -14
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +29 -2
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +1 -1
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-SYXOZQ4P.js.map +0 -1
- package/dist/chunk-XYRZV7R5.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -26,6 +26,50 @@ import { useEvents } from './useEvents';
|
|
|
26
26
|
import { applyPalette, clearPalette } from '../theming/runtime';
|
|
27
27
|
import type { PaletteData, ColorPalette } from '../theming/runtime';
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Parse and normalize event_colours to PaletteData (supports ev-* keys and string JSON)
|
|
31
|
+
* This matches the logic in EventService.parseAndNormalizeEventColours()
|
|
32
|
+
*/
|
|
33
|
+
function parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
|
|
34
|
+
try {
|
|
35
|
+
if (!input) return null;
|
|
36
|
+
let obj: any = input;
|
|
37
|
+
if (typeof input === 'string') {
|
|
38
|
+
try {
|
|
39
|
+
obj = JSON.parse(input);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
} else if (typeof input !== 'object') {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pick = (o: any, pref: string, plain: string) => (o?.[pref] ?? o?.[plain]) || null;
|
|
48
|
+
const main = pick(obj, 'ev-main', 'main');
|
|
49
|
+
const sec = pick(obj, 'ev-sec', 'sec');
|
|
50
|
+
const acc = pick(obj, 'ev-acc', 'acc');
|
|
51
|
+
if (!main && !sec && !acc) return null;
|
|
52
|
+
|
|
53
|
+
// Fill helper: return palette as-is, only return empty object if null/undefined/empty
|
|
54
|
+
const fill = (p: any) => {
|
|
55
|
+
if (!p) return {};
|
|
56
|
+
// If object is empty or has no actual color values, return empty object
|
|
57
|
+
const keys = Object.keys(p);
|
|
58
|
+
if (keys.length === 0) return {};
|
|
59
|
+
// Check if any values are truthy (not null/undefined)
|
|
60
|
+
const hasValues = keys.some(k => p[k] != null);
|
|
61
|
+
if (!hasValues) return {};
|
|
62
|
+
// Return the object as-is (don't fill missing shades)
|
|
63
|
+
return p;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return { main: fill(main), sec: fill(sec), acc: fill(acc) };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn('[useEventTheme] Failed to parse/normalize event colours:', error);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
/**
|
|
30
74
|
* Hook that automatically applies event-specific theming
|
|
31
75
|
*
|
|
@@ -62,30 +106,17 @@ export function useEventTheme(): void {
|
|
|
62
106
|
// Check if the event has theme colors
|
|
63
107
|
const eventColours = selectedEvent.event_colours;
|
|
64
108
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Validate that event_colours has the expected structure
|
|
71
|
-
const palette = eventColours as Partial<PaletteData>;
|
|
109
|
+
// Parse and normalize event_colours (same logic as EventService)
|
|
110
|
+
const normalized = parseAndNormalizeEventColours(eventColours);
|
|
72
111
|
|
|
73
|
-
|
|
74
|
-
if (!palette.main && !palette.sec && !palette.acc) {
|
|
112
|
+
if (!normalized) {
|
|
75
113
|
clearPalette();
|
|
76
114
|
return;
|
|
77
115
|
}
|
|
78
116
|
|
|
79
|
-
// Apply the palette
|
|
80
|
-
// The system expects main, sec, and acc, so we ensure all are present (empty if needed)
|
|
81
|
-
const fullPalette: PaletteData = {
|
|
82
|
-
main: (palette.main as ColorPalette) || {},
|
|
83
|
-
sec: (palette.sec as ColorPalette) || {},
|
|
84
|
-
acc: (palette.acc as ColorPalette) || {},
|
|
85
|
-
};
|
|
86
|
-
|
|
117
|
+
// Apply the normalized palette
|
|
87
118
|
try {
|
|
88
|
-
applyPalette(
|
|
119
|
+
applyPalette(normalized);
|
|
89
120
|
} catch (error) {
|
|
90
121
|
console.error('[useEventTheme] Failed to apply event palette:', error);
|
|
91
122
|
}
|
|
@@ -322,7 +322,8 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
322
322
|
}));
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
|
|
325
|
+
// Pre-size results array to maintain order and length
|
|
326
|
+
const results: PermissionResult[] = new Array(permissions.length);
|
|
326
327
|
const startTime = Date.now();
|
|
327
328
|
|
|
328
329
|
// Check cache for all permissions first
|
|
@@ -334,13 +335,14 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
334
335
|
const entry = cache.current.get(cacheKey);
|
|
335
336
|
|
|
336
337
|
if (entry && isCacheValid(entry)) {
|
|
337
|
-
|
|
338
|
+
// Use index assignment to maintain order
|
|
339
|
+
results[i] = {
|
|
338
340
|
operation,
|
|
339
341
|
pageId,
|
|
340
342
|
hasPermission: entry.result,
|
|
341
343
|
cached: true,
|
|
342
344
|
timestamp: entry.timestamp
|
|
343
|
-
}
|
|
345
|
+
};
|
|
344
346
|
stats.current.cacheHits++;
|
|
345
347
|
} else {
|
|
346
348
|
uncachedPermissions.push([operation, pageId, i]);
|
|
@@ -459,9 +459,19 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
459
459
|
await setOrganisationContextInSession(organisationId);
|
|
460
460
|
|
|
461
461
|
// Include organisation_id in RPC parameters
|
|
462
|
+
// Some functions use p_organisation_id instead of organisation_id (to avoid conflicts with RETURNS TABLE columns)
|
|
463
|
+
const functionsWithPOrganisationId = [
|
|
464
|
+
'data_cake_diners_list',
|
|
465
|
+
'data_cake_mealplans_list'
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const paramName = functionsWithPOrganisationId.includes(functionName)
|
|
469
|
+
? 'p_organisation_id'
|
|
470
|
+
: 'organisation_id';
|
|
471
|
+
|
|
462
472
|
const secureParams = {
|
|
463
473
|
...params,
|
|
464
|
-
|
|
474
|
+
[paramName]: organisationId
|
|
465
475
|
};
|
|
466
476
|
|
|
467
477
|
const { data, error } = await supabase!.rpc(functionName, secureParams);
|
package/src/hooks/useToast.ts
CHANGED
|
@@ -34,7 +34,7 @@ type ToasterToast = ToastProps & {
|
|
|
34
34
|
/** Unique identifier for the toast */
|
|
35
35
|
id: string
|
|
36
36
|
/** Duration before automatic dismissal in milliseconds */
|
|
37
|
-
duration
|
|
37
|
+
duration?: number
|
|
38
38
|
/** Optional title content */
|
|
39
39
|
title?: React.ReactNode
|
|
40
40
|
/** Optional description content */
|
|
@@ -52,16 +52,23 @@ export function EventServiceProvider({
|
|
|
52
52
|
|
|
53
53
|
// Update service dependencies and initialize when dependencies change
|
|
54
54
|
useEffect(() => {
|
|
55
|
-
eventService.updateDependencies(supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId);
|
|
56
|
-
|
|
57
|
-
// Re-initialize service when dependencies change
|
|
58
55
|
let isMounted = true;
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const updateAndInitialize = async () => {
|
|
58
|
+
// Update dependencies (now async to handle user change cleanup)
|
|
59
|
+
await eventService.updateDependencies(supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId);
|
|
60
|
+
|
|
61
|
+
if (!isMounted) return;
|
|
62
|
+
|
|
63
|
+
// Re-initialize service when dependencies change
|
|
64
|
+
await eventService.initialize().catch(error => {
|
|
65
|
+
if (isMounted) {
|
|
66
|
+
console.error('[EventServiceProvider] Failed to initialize event service:', error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
updateAndInitialize();
|
|
65
72
|
|
|
66
73
|
return () => {
|
|
67
74
|
isMounted = false;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RBAC Cache Invalidation Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/CacheInvalidation
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for the RBAC cache invalidation system.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
11
|
+
import { createMockSupabaseClient } from '@test/helpers';
|
|
12
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
13
|
+
import type { Database } from '../types/database';
|
|
14
|
+
import {
|
|
15
|
+
RBACCacheInvalidationManager,
|
|
16
|
+
INVALIDATION_PATTERNS,
|
|
17
|
+
initializeCacheInvalidation,
|
|
18
|
+
getCacheInvalidationManager,
|
|
19
|
+
} from '../cache-invalidation';
|
|
20
|
+
import { rbacCache, CACHE_PATTERNS } from '../cache';
|
|
21
|
+
import { emitAuditEvent } from '../audit';
|
|
22
|
+
|
|
23
|
+
// Mock the cache module
|
|
24
|
+
vi.mock('../cache', () => ({
|
|
25
|
+
rbacCache: {
|
|
26
|
+
invalidate: vi.fn(),
|
|
27
|
+
clear: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
CACHE_PATTERNS: {
|
|
30
|
+
USER: (userId: string) => `user:${userId}`,
|
|
31
|
+
ORGANISATION: (orgId: string) => `org:${orgId}`,
|
|
32
|
+
EVENT: (eventId: string) => `event:${eventId}`,
|
|
33
|
+
APP: (appId: string) => `app:${appId}`,
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Mock the audit module
|
|
38
|
+
vi.mock('../audit', () => ({
|
|
39
|
+
emitAuditEvent: vi.fn(() => Promise.resolve(undefined)),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('RBAC Cache Invalidation', () => {
|
|
43
|
+
let mockSupabase: ReturnType<typeof createMockSupabaseClient>;
|
|
44
|
+
let manager: RBACCacheInvalidationManager;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
mockSupabase = createMockSupabaseClient();
|
|
49
|
+
manager = new RBACCacheInvalidationManager(mockSupabase as unknown as SupabaseClient<Database>);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('INVALIDATION_PATTERNS', () => {
|
|
57
|
+
describe('USER_ROLES_CHANGED', () => {
|
|
58
|
+
it('generates correct patterns for user invalidation', () => {
|
|
59
|
+
const userId = 'user-123' as const;
|
|
60
|
+
const patterns = INVALIDATION_PATTERNS.USER_ROLES_CHANGED(userId);
|
|
61
|
+
|
|
62
|
+
expect(patterns).toContain('user:user-123');
|
|
63
|
+
expect(patterns).toContain('perm:user-123:*');
|
|
64
|
+
expect(patterns).toContain('access:user-123:*');
|
|
65
|
+
expect(patterns).toContain('map:user-123:*');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('ORGANISATION_PERMISSIONS_CHANGED', () => {
|
|
70
|
+
it('generates correct patterns for organisation invalidation', () => {
|
|
71
|
+
const orgId = 'org-456' as const;
|
|
72
|
+
const patterns = INVALIDATION_PATTERNS.ORGANISATION_PERMISSIONS_CHANGED(orgId);
|
|
73
|
+
|
|
74
|
+
expect(patterns).toContain('org:org-456');
|
|
75
|
+
expect(patterns).toContain('perm:*:org-456:*');
|
|
76
|
+
expect(patterns).toContain('access:*:org-456:*');
|
|
77
|
+
expect(patterns).toContain('map:*:org-456:*');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('EVENT_PERMISSIONS_CHANGED', () => {
|
|
82
|
+
it('generates correct patterns for event invalidation', () => {
|
|
83
|
+
const eventId = 'event-789';
|
|
84
|
+
const patterns = INVALIDATION_PATTERNS.EVENT_PERMISSIONS_CHANGED(eventId);
|
|
85
|
+
|
|
86
|
+
expect(patterns).toContain('event:event-789');
|
|
87
|
+
expect(patterns).toContain('perm:*:*:event-789:*');
|
|
88
|
+
expect(patterns).toContain('access:*:*:event-789:*');
|
|
89
|
+
expect(patterns).toContain('map:*:*:event-789:*');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('APP_PERMISSIONS_CHANGED', () => {
|
|
94
|
+
it('generates correct patterns for app invalidation', () => {
|
|
95
|
+
const appId = 'app-abc' as const;
|
|
96
|
+
const patterns = INVALIDATION_PATTERNS.APP_PERMISSIONS_CHANGED(appId);
|
|
97
|
+
|
|
98
|
+
expect(patterns).toContain('app:app-abc');
|
|
99
|
+
expect(patterns).toContain('perm:*:*:*:app-abc:*');
|
|
100
|
+
expect(patterns).toContain('access:*:*:*:app-abc');
|
|
101
|
+
expect(patterns).toContain('map:*:*:*:app-abc');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('PAGE_PERMISSIONS_CHANGED', () => {
|
|
106
|
+
it('generates correct patterns for page invalidation', () => {
|
|
107
|
+
const pageId = 'page-xyz' as const;
|
|
108
|
+
const patterns = INVALIDATION_PATTERNS.PAGE_PERMISSIONS_CHANGED(pageId);
|
|
109
|
+
|
|
110
|
+
expect(patterns).toContain('perm:*:*:*:*:page-xyz');
|
|
111
|
+
expect(patterns).toContain('map:*:*:*:*');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('RBACCacheInvalidationManager', () => {
|
|
117
|
+
describe('onInvalidation', () => {
|
|
118
|
+
it('adds callback and returns unsubscribe function', () => {
|
|
119
|
+
const callback = vi.fn();
|
|
120
|
+
const unsubscribe = manager.onInvalidation(callback);
|
|
121
|
+
|
|
122
|
+
expect(typeof unsubscribe).toBe('function');
|
|
123
|
+
|
|
124
|
+
// Trigger invalidation
|
|
125
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
126
|
+
|
|
127
|
+
expect(callback).toHaveBeenCalled();
|
|
128
|
+
|
|
129
|
+
// Unsubscribe
|
|
130
|
+
unsubscribe();
|
|
131
|
+
|
|
132
|
+
// Clear mocks
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
|
|
135
|
+
// Trigger again - callback should not be called
|
|
136
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
137
|
+
expect(callback).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles multiple callbacks', () => {
|
|
141
|
+
const callback1 = vi.fn();
|
|
142
|
+
const callback2 = vi.fn();
|
|
143
|
+
|
|
144
|
+
manager.onInvalidation(callback1);
|
|
145
|
+
manager.onInvalidation(callback2);
|
|
146
|
+
|
|
147
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
148
|
+
|
|
149
|
+
expect(callback1).toHaveBeenCalled();
|
|
150
|
+
expect(callback2).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('invalidateUser', () => {
|
|
155
|
+
it('invalidates cache for user with correct patterns', () => {
|
|
156
|
+
const userId = 'user-123' as const;
|
|
157
|
+
manager.invalidateUser(userId, 'test-reason');
|
|
158
|
+
|
|
159
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-123');
|
|
160
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:user-123:*');
|
|
161
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:user-123:*');
|
|
162
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:user-123:*');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('emits audit event for user invalidation', async () => {
|
|
166
|
+
const userId = 'user-123' as const;
|
|
167
|
+
manager.invalidateUser(userId, 'test-reason');
|
|
168
|
+
|
|
169
|
+
await vi.waitFor(() => {
|
|
170
|
+
expect(emitAuditEvent).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const auditCall = vi.mocked(emitAuditEvent).mock.calls[0][0];
|
|
174
|
+
expect(auditCall.type).toBe('permission_check');
|
|
175
|
+
expect(auditCall.permission).toBe('cache:invalidate');
|
|
176
|
+
expect(auditCall.metadata?.reason).toBe('test-reason');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('invalidateOrganisation', () => {
|
|
181
|
+
it('invalidates cache for organisation with correct patterns', () => {
|
|
182
|
+
const orgId = 'org-456' as const;
|
|
183
|
+
manager.invalidateOrganisation(orgId, 'test-reason');
|
|
184
|
+
|
|
185
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('org:org-456');
|
|
186
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:org-456:*');
|
|
187
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:org-456:*');
|
|
188
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:org-456:*');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('invalidateEvent', () => {
|
|
193
|
+
it('invalidates cache for event with correct patterns', () => {
|
|
194
|
+
const eventId = 'event-789';
|
|
195
|
+
manager.invalidateEvent(eventId, 'test-reason');
|
|
196
|
+
|
|
197
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('event:event-789');
|
|
198
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:event-789:*');
|
|
199
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:*:event-789:*');
|
|
200
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:event-789:*');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('invalidateApp', () => {
|
|
205
|
+
it('invalidates cache for app with correct patterns', () => {
|
|
206
|
+
const appId = 'app-abc' as const;
|
|
207
|
+
manager.invalidateApp(appId, 'test-reason');
|
|
208
|
+
|
|
209
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('app:app-abc');
|
|
210
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:*:app-abc:*');
|
|
211
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:*:*:app-abc');
|
|
212
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:*:app-abc');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('invalidatePage', () => {
|
|
217
|
+
it('invalidates cache for page with correct patterns', () => {
|
|
218
|
+
const pageId = 'page-xyz' as const;
|
|
219
|
+
manager.invalidatePage(pageId, 'test-reason');
|
|
220
|
+
|
|
221
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:*:*:page-xyz');
|
|
222
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:*:*');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('invalidateAllUsersInOrganisation', () => {
|
|
227
|
+
it('invalidates cache for all users in organisation', async () => {
|
|
228
|
+
const orgId = 'org-456' as const;
|
|
229
|
+
const mockUsers = [
|
|
230
|
+
{ user_id: 'user-1' as const },
|
|
231
|
+
{ user_id: 'user-2' as const },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
mockSupabase.from.mockReturnValue({
|
|
235
|
+
select: vi.fn().mockReturnValue({
|
|
236
|
+
eq: vi.fn().mockReturnValue({
|
|
237
|
+
eq: vi.fn().mockResolvedValue({
|
|
238
|
+
data: mockUsers,
|
|
239
|
+
error: null,
|
|
240
|
+
}),
|
|
241
|
+
}),
|
|
242
|
+
}),
|
|
243
|
+
} as any);
|
|
244
|
+
|
|
245
|
+
await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
|
|
246
|
+
|
|
247
|
+
// Should invalidate each user
|
|
248
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-1');
|
|
249
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-2');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('handles empty user list gracefully', async () => {
|
|
253
|
+
const orgId = 'org-456' as const;
|
|
254
|
+
|
|
255
|
+
mockSupabase.from.mockReturnValue({
|
|
256
|
+
select: vi.fn().mockReturnValue({
|
|
257
|
+
eq: vi.fn().mockReturnValue({
|
|
258
|
+
eq: vi.fn().mockResolvedValue({
|
|
259
|
+
data: [],
|
|
260
|
+
error: null,
|
|
261
|
+
}),
|
|
262
|
+
}),
|
|
263
|
+
}),
|
|
264
|
+
} as any);
|
|
265
|
+
|
|
266
|
+
await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
|
|
267
|
+
|
|
268
|
+
// Should not throw, but should not call invalidate for users
|
|
269
|
+
expect(rbacCache.invalidate).not.toHaveBeenCalledWith(expect.stringContaining('user:'));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('handles null data gracefully', async () => {
|
|
273
|
+
const orgId = 'org-456' as const;
|
|
274
|
+
|
|
275
|
+
mockSupabase.from.mockReturnValue({
|
|
276
|
+
select: vi.fn().mockReturnValue({
|
|
277
|
+
eq: vi.fn().mockReturnValue({
|
|
278
|
+
eq: vi.fn().mockResolvedValue({
|
|
279
|
+
data: null,
|
|
280
|
+
error: null,
|
|
281
|
+
}),
|
|
282
|
+
}),
|
|
283
|
+
}),
|
|
284
|
+
} as any);
|
|
285
|
+
|
|
286
|
+
await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
|
|
287
|
+
|
|
288
|
+
// Should not throw
|
|
289
|
+
expect(rbacCache.invalidate).not.toHaveBeenCalledWith(expect.stringContaining('user:'));
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('clearAllCache', () => {
|
|
294
|
+
it('clears all cache entries', () => {
|
|
295
|
+
manager.clearAllCache();
|
|
296
|
+
expect(rbacCache.clear).toHaveBeenCalled();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('setupRealtimeSubscriptions', () => {
|
|
301
|
+
it('skips subscriptions when realtime is not available', () => {
|
|
302
|
+
const mockSupabaseNoRealtime = createMockSupabaseClient();
|
|
303
|
+
mockSupabaseNoRealtime.channel = undefined;
|
|
304
|
+
|
|
305
|
+
const managerNoRealtime = new RBACCacheInvalidationManager(
|
|
306
|
+
mockSupabaseNoRealtime as unknown as SupabaseClient<Database>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Should not throw and should log that realtime is not available
|
|
310
|
+
expect(managerNoRealtime).toBeDefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('sets up subscriptions when realtime is available', () => {
|
|
314
|
+
const mockChannel = {
|
|
315
|
+
on: vi.fn().mockReturnThis(),
|
|
316
|
+
subscribe: vi.fn(),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const mockSupabaseWithRealtime = createMockSupabaseClient();
|
|
320
|
+
mockSupabaseWithRealtime.channel = vi.fn(() => mockChannel);
|
|
321
|
+
|
|
322
|
+
const managerWithRealtime = new RBACCacheInvalidationManager(
|
|
323
|
+
mockSupabaseWithRealtime as unknown as SupabaseClient<Database>
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(mockSupabaseWithRealtime.channel).toHaveBeenCalled();
|
|
327
|
+
expect(mockChannel.on).toHaveBeenCalled();
|
|
328
|
+
expect(mockChannel.subscribe).toHaveBeenCalled();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('Global Functions', () => {
|
|
334
|
+
describe('initializeCacheInvalidation', () => {
|
|
335
|
+
it('initializes and returns cache invalidation manager', () => {
|
|
336
|
+
const manager = initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
|
|
337
|
+
|
|
338
|
+
expect(manager).toBeInstanceOf(RBACCacheInvalidationManager);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('sets global manager instance', () => {
|
|
342
|
+
initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
|
|
343
|
+
|
|
344
|
+
const globalManager = getCacheInvalidationManager();
|
|
345
|
+
expect(globalManager).toBeInstanceOf(RBACCacheInvalidationManager);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('getCacheInvalidationManager', () => {
|
|
350
|
+
it('returns null when manager is not initialized', () => {
|
|
351
|
+
// Reset global state by creating new instance without initialize
|
|
352
|
+
const manager = getCacheInvalidationManager();
|
|
353
|
+
// May be null or the previous instance
|
|
354
|
+
expect(manager === null || manager instanceof RBACCacheInvalidationManager).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('returns manager instance after initialization', () => {
|
|
358
|
+
const manager = initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
|
|
359
|
+
const retrieved = getCacheInvalidationManager();
|
|
360
|
+
|
|
361
|
+
expect(retrieved).toBe(manager);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('Error Handling', () => {
|
|
367
|
+
it('handles audit event errors gracefully', async () => {
|
|
368
|
+
vi.mocked(emitAuditEvent).mockRejectedValue(new Error('Audit error'));
|
|
369
|
+
|
|
370
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
371
|
+
|
|
372
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
373
|
+
|
|
374
|
+
await vi.waitFor(() => {
|
|
375
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
376
|
+
expect.stringContaining('[RBAC Cache] Failed to log cache invalidation audit event'),
|
|
377
|
+
expect.any(Error)
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
consoleWarnSpy.mockRestore();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|