@jmruthers/pace-core 0.5.19 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-ZEQDWNKA.js → DataTable-IX7N7XYG.js} +4 -4
- package/dist/{chunk-3ZVV6URZ.js → chunk-37WAIATW.js} +4 -4
- package/dist/{chunk-7UEIZCST.js → chunk-4TMM2IGR.js} +5 -4
- package/dist/chunk-4TMM2IGR.js.map +1 -0
- package/dist/{chunk-DKDTXS5Q.js → chunk-JE7K6Q3N.js} +10 -9
- package/dist/chunk-JE7K6Q3N.js.map +1 -0
- package/dist/{chunk-CLEIIMJO.js → chunk-R4QSIQDW.js} +2 -2
- package/dist/{chunk-J42NVDVM.js → chunk-ULP6OIQE.js} +42 -8
- package/dist/chunk-ULP6OIQE.js.map +1 -0
- package/dist/{chunk-CMXBNDPM.js → chunk-UYKKHRJN.js} +2 -2
- package/dist/{chunk-7EGP6KZT.js → chunk-YA77BOZM.js} +17 -17
- package/dist/chunk-YA77BOZM.js.map +1 -0
- package/dist/{chunk-DV2Z3RBQ.js → chunk-ZIYFAQJ5.js} +2 -2
- package/dist/components.js +6 -6
- package/dist/hooks.js +2 -2
- package/dist/index.js +8 -8
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +3 -3
- package/dist/utils.js +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 +1 -1
- 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/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/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/EventContextType.md +7 -7
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +2 -2
- package/docs/api/interfaces/FileSizeLimits.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 +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +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/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/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- 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/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/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.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 +12 -12
- package/docs/troubleshooting/cake-infinite-rerender-debugging.md +284 -0
- package/docs/troubleshooting/cake-infinite-rerender-summary.md +117 -0
- package/docs/troubleshooting/cake-rerender-diagnostic.js +162 -0
- package/docs/troubleshooting/rbac-critical-fixes-summary.md +260 -0
- package/package.json +1 -1
- package/src/__tests__/hooks/usePermissions.test.ts +265 -0
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +187 -0
- package/src/hooks/useAppConfig.ts +7 -6
- package/src/providers/EventProvider.tsx +4 -2
- package/src/rbac/components/PagePermissionGuard.tsx +56 -13
- package/src/rbac/hooks/usePermissions.ts +21 -14
- package/dist/chunk-7EGP6KZT.js.map +0 -1
- package/dist/chunk-7UEIZCST.js.map +0 -1
- package/dist/chunk-DKDTXS5Q.js.map +0 -1
- package/dist/chunk-J42NVDVM.js.map +0 -1
- /package/dist/{DataTable-ZEQDWNKA.js.map → DataTable-IX7N7XYG.js.map} +0 -0
- /package/dist/{chunk-3ZVV6URZ.js.map → chunk-37WAIATW.js.map} +0 -0
- /package/dist/{chunk-CLEIIMJO.js.map → chunk-R4QSIQDW.js.map} +0 -0
- /package/dist/{chunk-CMXBNDPM.js.map → chunk-UYKKHRJN.js.map} +0 -0
- /package/dist/{chunk-DV2Z3RBQ.js.map → chunk-ZIYFAQJ5.js.map} +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file PagePermissionGuard Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/Components/PagePermissionGuard
|
|
5
|
+
* @since 2.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for PagePermissionGuard component to ensure:
|
|
8
|
+
* - UI state management works correctly
|
|
9
|
+
* - Permission checks are properly handled
|
|
10
|
+
* - No infinite re-renders occur
|
|
11
|
+
* - Scope resolution is robust
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { render, screen, waitFor, act } from '@testing-library/react';
|
|
16
|
+
import { vi } from 'vitest';
|
|
17
|
+
import { PagePermissionGuard } from '../../rbac/components/PagePermissionGuard';
|
|
18
|
+
import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
|
|
19
|
+
import { useCan } from '../../rbac/hooks/usePermissions';
|
|
20
|
+
|
|
21
|
+
// Mock the hooks
|
|
22
|
+
vi.mock('../../providers/UnifiedAuthProvider');
|
|
23
|
+
vi.mock('../../rbac/hooks/usePermissions');
|
|
24
|
+
vi.mock('../../utils/appNameResolver');
|
|
25
|
+
|
|
26
|
+
const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
|
|
27
|
+
const mockUseCan = vi.mocked(useCan);
|
|
28
|
+
|
|
29
|
+
// Mock app name resolver
|
|
30
|
+
vi.mock('../../utils/appNameResolver', () => ({
|
|
31
|
+
getCurrentAppName: () => 'test-app'
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe('PagePermissionGuard', () => {
|
|
35
|
+
const mockUser = {
|
|
36
|
+
id: 'user-123',
|
|
37
|
+
email: 'test@example.com'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mockScope = {
|
|
41
|
+
organisationId: 'org-123',
|
|
42
|
+
eventId: 'event-123',
|
|
43
|
+
appId: 'app-123'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
// Reset all mocks
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
|
|
50
|
+
// Default mock implementations
|
|
51
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
52
|
+
user: mockUser,
|
|
53
|
+
selectedOrganisationId: 'org-123',
|
|
54
|
+
selectedEventId: 'event-123',
|
|
55
|
+
supabase: {} as any,
|
|
56
|
+
session: {} as any,
|
|
57
|
+
appName: 'test-app',
|
|
58
|
+
isAuthenticated: true,
|
|
59
|
+
signOut: vi.fn(),
|
|
60
|
+
// Add other required properties
|
|
61
|
+
} as any);
|
|
62
|
+
|
|
63
|
+
mockUseCan.mockReturnValue({
|
|
64
|
+
can: false,
|
|
65
|
+
isLoading: false,
|
|
66
|
+
error: null,
|
|
67
|
+
refetch: vi.fn()
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('UI State Management', () => {
|
|
72
|
+
it('should render without crashing', () => {
|
|
73
|
+
render(
|
|
74
|
+
<PagePermissionGuard pageName="test" operation="read">
|
|
75
|
+
<div>Protected Content</div>
|
|
76
|
+
</PagePermissionGuard>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Should render something (either loading, content, or fallback)
|
|
80
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should show protected content when permission is granted', async () => {
|
|
84
|
+
mockUseCan.mockReturnValue({
|
|
85
|
+
can: true,
|
|
86
|
+
isLoading: false,
|
|
87
|
+
error: null,
|
|
88
|
+
refetch: vi.fn()
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
render(
|
|
92
|
+
<PagePermissionGuard pageName="test" operation="read">
|
|
93
|
+
<div>Protected Content</div>
|
|
94
|
+
</PagePermissionGuard>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Permission State Transitions', () => {
|
|
104
|
+
it('should handle permission state changes', async () => {
|
|
105
|
+
mockUseCan.mockReturnValue({
|
|
106
|
+
can: true,
|
|
107
|
+
isLoading: false,
|
|
108
|
+
error: null,
|
|
109
|
+
refetch: vi.fn()
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
render(
|
|
113
|
+
<PagePermissionGuard pageName="test" operation="read">
|
|
114
|
+
<div>Protected Content</div>
|
|
115
|
+
</PagePermissionGuard>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('Error Handling', () => {
|
|
125
|
+
it('should handle errors gracefully', async () => {
|
|
126
|
+
mockUseCan.mockReturnValue({
|
|
127
|
+
can: false,
|
|
128
|
+
isLoading: false,
|
|
129
|
+
error: new Error('Permission check failed'),
|
|
130
|
+
refetch: vi.fn()
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
render(
|
|
134
|
+
<PagePermissionGuard pageName="test" operation="read">
|
|
135
|
+
<div>Protected Content</div>
|
|
136
|
+
</PagePermissionGuard>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Should render something even with errors
|
|
140
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Hook Stability', () => {
|
|
145
|
+
it('should render without infinite re-renders', () => {
|
|
146
|
+
let renderCount = 0;
|
|
147
|
+
|
|
148
|
+
const TestComponent = () => {
|
|
149
|
+
renderCount++;
|
|
150
|
+
return (
|
|
151
|
+
<PagePermissionGuard pageName="test" operation="read">
|
|
152
|
+
<div>Protected Content</div>
|
|
153
|
+
</PagePermissionGuard>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
render(<TestComponent />);
|
|
158
|
+
|
|
159
|
+
// Should not have excessive re-renders
|
|
160
|
+
expect(renderCount).toBeLessThan(10);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('Scope Resolution', () => {
|
|
165
|
+
it('should work with organisation context', async () => {
|
|
166
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
167
|
+
user: mockUser,
|
|
168
|
+
selectedOrganisationId: 'org-123',
|
|
169
|
+
selectedEventId: null,
|
|
170
|
+
supabase: {} as any,
|
|
171
|
+
session: {} as any,
|
|
172
|
+
appName: 'test-app',
|
|
173
|
+
isAuthenticated: true,
|
|
174
|
+
signOut: vi.fn(),
|
|
175
|
+
} as any);
|
|
176
|
+
|
|
177
|
+
render(
|
|
178
|
+
<PagePermissionGuard pageName="test" operation="read">
|
|
179
|
+
<div>Protected Content</div>
|
|
180
|
+
</PagePermissionGuard>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Should render something
|
|
184
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { useMemo } from 'react';
|
|
31
32
|
import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
|
|
32
33
|
import { useIsPublicPage } from '../components/PublicLayout/PublicPageProvider';
|
|
33
34
|
|
|
@@ -65,30 +66,30 @@ export function useAppConfig(): UseAppConfigReturn {
|
|
|
65
66
|
return 'PACE';
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
return {
|
|
69
|
+
return useMemo(() => ({
|
|
69
70
|
supportsDirectAccess: false, // Public pages don't support direct access
|
|
70
71
|
requiresEvent: true, // Public pages always require an event
|
|
71
72
|
isLoading: false,
|
|
72
73
|
appName: getAppName()
|
|
73
|
-
};
|
|
74
|
+
}), []);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
// For authenticated pages, use UnifiedAuthProvider
|
|
77
78
|
try {
|
|
78
79
|
const { appConfig, appName } = useUnifiedAuth();
|
|
79
|
-
return {
|
|
80
|
+
return useMemo(() => ({
|
|
80
81
|
supportsDirectAccess: !(appConfig?.requires_event ?? true),
|
|
81
82
|
requiresEvent: appConfig?.requires_event ?? true,
|
|
82
83
|
isLoading: appConfig === null,
|
|
83
84
|
appName
|
|
84
|
-
};
|
|
85
|
+
}), [appConfig?.requires_event, appName]);
|
|
85
86
|
} catch (error) {
|
|
86
87
|
// Fallback if UnifiedAuthProvider is not available
|
|
87
|
-
return {
|
|
88
|
+
return useMemo(() => ({
|
|
88
89
|
supportsDirectAccess: false,
|
|
89
90
|
requiresEvent: true,
|
|
90
91
|
isLoading: false,
|
|
91
92
|
appName: 'PACE'
|
|
92
|
-
};
|
|
93
|
+
}), []);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
@@ -6,6 +6,7 @@ import React, {
|
|
|
6
6
|
useLayoutEffect,
|
|
7
7
|
useCallback,
|
|
8
8
|
useRef,
|
|
9
|
+
useMemo,
|
|
9
10
|
} from 'react';
|
|
10
11
|
import { useUnifiedAuth } from './UnifiedAuthProvider';
|
|
11
12
|
import { useOrganisations } from './OrganisationProvider';
|
|
@@ -307,14 +308,15 @@ export function EventProvider({ children }: EventProviderProps) {
|
|
|
307
308
|
await fetchEvents();
|
|
308
309
|
}, [fetchEvents]);
|
|
309
310
|
|
|
310
|
-
|
|
311
|
+
// Memoize the context value to prevent unnecessary re-renders
|
|
312
|
+
const contextValue: EventContextType = useMemo(() => ({
|
|
311
313
|
events,
|
|
312
314
|
selectedEvent,
|
|
313
315
|
isLoading,
|
|
314
316
|
error,
|
|
315
317
|
setSelectedEvent,
|
|
316
318
|
refreshEvents,
|
|
317
|
-
};
|
|
319
|
+
}), [events, selectedEvent, isLoading, error, setSelectedEvent, refreshEvents]);
|
|
318
320
|
|
|
319
321
|
return (
|
|
320
322
|
<EventContext.Provider value={contextValue}>
|
|
@@ -319,8 +319,19 @@ const PagePermissionGuardComponent = ({
|
|
|
319
319
|
return;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
// No context available
|
|
323
|
-
|
|
322
|
+
// No context available - provide more helpful error message
|
|
323
|
+
const errorMessage = !selectedOrganisationId && !selectedEventId
|
|
324
|
+
? 'Either organisation context or event context is required for page permission checking'
|
|
325
|
+
: 'Insufficient context for permission checking. Please ensure you are properly authenticated and have selected an organisation or event.';
|
|
326
|
+
|
|
327
|
+
console.error('[PagePermissionGuard] Context resolution failed:', {
|
|
328
|
+
selectedOrganisationId,
|
|
329
|
+
selectedEventId,
|
|
330
|
+
appId,
|
|
331
|
+
error: errorMessage
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
setCheckError(new Error(errorMessage));
|
|
324
335
|
setResolvedScope(null); // Ensure we don't proceed with incomplete scope
|
|
325
336
|
};
|
|
326
337
|
|
|
@@ -355,7 +366,7 @@ const PagePermissionGuardComponent = ({
|
|
|
355
366
|
useEffect(() => {
|
|
356
367
|
if (!isLoading && !error) {
|
|
357
368
|
setHasChecked(true);
|
|
358
|
-
setCheckError(null);
|
|
369
|
+
setCheckError(null); // Clear any previous errors when permission check succeeds
|
|
359
370
|
|
|
360
371
|
if (!can && onDenied) {
|
|
361
372
|
onDenied(pageName, operation);
|
|
@@ -393,35 +404,67 @@ const PagePermissionGuardComponent = ({
|
|
|
393
404
|
}
|
|
394
405
|
}, [strictMode, hasChecked, isLoading, can, pageName, operation, user?.id, resolvedScope]);
|
|
395
406
|
|
|
396
|
-
// Calculate the actual render state
|
|
397
|
-
|
|
398
|
-
const
|
|
407
|
+
// Calculate the actual render state - FIXED: Proper state calculation
|
|
408
|
+
// Add defensive checks to ensure we have valid state
|
|
409
|
+
const hasValidScope = resolvedScope && resolvedScope.organisationId;
|
|
410
|
+
const hasValidUser = user && user.id;
|
|
411
|
+
const isPermissionCheckComplete = hasChecked && !isLoading;
|
|
412
|
+
|
|
413
|
+
const shouldShowAccessDenied = isPermissionCheckComplete && hasValidScope && hasValidUser && !checkError && !can;
|
|
414
|
+
const shouldShowContent = isPermissionCheckComplete && hasValidScope && hasValidUser && !checkError && can;
|
|
399
415
|
|
|
400
416
|
// Create a key to force re-render when scope or permission state changes
|
|
401
417
|
const scopeKey = resolvedScope ? `${resolvedScope.organisationId}-${resolvedScope.eventId}-${resolvedScope.appId}` : 'no-scope';
|
|
402
|
-
const permissionKey = `${scopeKey}-${can}-${isLoading}-${!!checkError}`;
|
|
418
|
+
const permissionKey = `${scopeKey}-${can}-${isLoading}-${!!checkError}-${hasChecked}`;
|
|
403
419
|
|
|
404
|
-
//
|
|
405
|
-
|
|
420
|
+
// Debug logging for state transitions
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
console.log('[PagePermissionGuard] State transition:', {
|
|
423
|
+
instanceId,
|
|
424
|
+
isLoading,
|
|
425
|
+
hasChecked,
|
|
426
|
+
resolvedScope: !!resolvedScope,
|
|
427
|
+
checkError: !!checkError,
|
|
428
|
+
can,
|
|
429
|
+
shouldShowAccessDenied,
|
|
430
|
+
shouldShowContent,
|
|
431
|
+
permissionKey
|
|
432
|
+
});
|
|
433
|
+
}, [isLoading, hasChecked, resolvedScope, checkError, can, shouldShowAccessDenied, shouldShowContent, permissionKey, instanceId]);
|
|
434
|
+
|
|
435
|
+
// Show loading state - if we're still loading or don't have valid state
|
|
436
|
+
if (isLoading || !hasValidScope || !hasValidUser || !hasChecked) {
|
|
406
437
|
return <div key={`loading-${permissionKey}`}>{loading}</div>;
|
|
407
438
|
}
|
|
408
439
|
|
|
409
|
-
// Show error state
|
|
410
|
-
if (checkError) {
|
|
440
|
+
// Show error state - only if we have an error AND no permission
|
|
441
|
+
if (checkError && !can) {
|
|
411
442
|
return <div key={`error-${permissionKey}`}>{fallback}</div>;
|
|
412
443
|
}
|
|
413
444
|
|
|
414
|
-
// Show access denied
|
|
445
|
+
// Show access denied - if permission check is complete and user doesn't have permission
|
|
415
446
|
if (shouldShowAccessDenied) {
|
|
416
447
|
return <div key={`denied-${permissionKey}`}>{fallback}</div>;
|
|
417
448
|
}
|
|
418
449
|
|
|
419
|
-
// Show protected content
|
|
450
|
+
// Show protected content - if permission check is complete and user has permission
|
|
420
451
|
if (shouldShowContent) {
|
|
421
452
|
return <div key={`content-${permissionKey}`}>{children}</div>;
|
|
422
453
|
}
|
|
423
454
|
|
|
424
455
|
// Fallback: This should never happen, but just in case
|
|
456
|
+
// If we get here, something went wrong with our state logic
|
|
457
|
+
console.warn('[PagePermissionGuard] Unexpected state - falling back to loading', {
|
|
458
|
+
instanceId,
|
|
459
|
+
isLoading,
|
|
460
|
+
hasValidScope,
|
|
461
|
+
hasValidUser,
|
|
462
|
+
hasChecked,
|
|
463
|
+
checkError: !!checkError,
|
|
464
|
+
can,
|
|
465
|
+
shouldShowAccessDenied,
|
|
466
|
+
shouldShowContent
|
|
467
|
+
});
|
|
425
468
|
return <div key={`fallback-${permissionKey}`}>{loading}</div>;
|
|
426
469
|
}
|
|
427
470
|
|
|
@@ -139,7 +139,8 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
139
139
|
}
|
|
140
140
|
}, [userId, scope.organisationId, scope.eventId, scope.appId]);
|
|
141
141
|
|
|
142
|
-
return
|
|
142
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
143
|
+
return useMemo(() => ({
|
|
143
144
|
permissions,
|
|
144
145
|
isLoading,
|
|
145
146
|
error,
|
|
@@ -147,7 +148,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
147
148
|
hasAnyPermission,
|
|
148
149
|
hasAllPermissions,
|
|
149
150
|
refetch
|
|
150
|
-
};
|
|
151
|
+
}), [permissions, isLoading, error, hasPermission, hasAnyPermission, hasAllPermissions, refetch]);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
/**
|
|
@@ -276,12 +277,13 @@ export function useCan(
|
|
|
276
277
|
}
|
|
277
278
|
}, [userId, scope.organisationId, scope.eventId, scope.appId, permission, pageId, useCache]);
|
|
278
279
|
|
|
279
|
-
return
|
|
280
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
281
|
+
return useMemo(() => ({
|
|
280
282
|
can,
|
|
281
283
|
isLoading,
|
|
282
284
|
error,
|
|
283
285
|
refetch
|
|
284
|
-
};
|
|
286
|
+
}), [can, isLoading, error, refetch]);
|
|
285
287
|
}
|
|
286
288
|
|
|
287
289
|
/**
|
|
@@ -343,12 +345,13 @@ export function useAccessLevel(userId: UUID, scope: Scope): {
|
|
|
343
345
|
fetchAccessLevel();
|
|
344
346
|
}, [fetchAccessLevel]);
|
|
345
347
|
|
|
346
|
-
return
|
|
348
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
349
|
+
return useMemo(() => ({
|
|
347
350
|
accessLevel,
|
|
348
351
|
isLoading,
|
|
349
352
|
error,
|
|
350
353
|
refetch: fetchAccessLevel
|
|
351
|
-
};
|
|
354
|
+
}), [accessLevel, isLoading, error, fetchAccessLevel]);
|
|
352
355
|
}
|
|
353
356
|
|
|
354
357
|
/**
|
|
@@ -431,12 +434,13 @@ export function useMultiplePermissions(
|
|
|
431
434
|
checkPermissions();
|
|
432
435
|
}, [checkPermissions]);
|
|
433
436
|
|
|
434
|
-
return
|
|
437
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
438
|
+
return useMemo(() => ({
|
|
435
439
|
results,
|
|
436
440
|
isLoading,
|
|
437
441
|
error,
|
|
438
442
|
refetch: checkPermissions
|
|
439
|
-
};
|
|
443
|
+
}), [results, isLoading, error, checkPermissions]);
|
|
440
444
|
}
|
|
441
445
|
|
|
442
446
|
/**
|
|
@@ -516,12 +520,13 @@ export function useHasAnyPermission(
|
|
|
516
520
|
checkAnyPermission();
|
|
517
521
|
}, [checkAnyPermission]);
|
|
518
522
|
|
|
519
|
-
return
|
|
523
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
524
|
+
return useMemo(() => ({
|
|
520
525
|
hasAny,
|
|
521
526
|
isLoading,
|
|
522
527
|
error,
|
|
523
528
|
refetch: checkAnyPermission
|
|
524
|
-
};
|
|
529
|
+
}), [hasAny, isLoading, error, checkAnyPermission]);
|
|
525
530
|
}
|
|
526
531
|
|
|
527
532
|
/**
|
|
@@ -601,12 +606,13 @@ export function useHasAllPermissions(
|
|
|
601
606
|
checkAllPermissions();
|
|
602
607
|
}, [checkAllPermissions]);
|
|
603
608
|
|
|
604
|
-
return
|
|
609
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
610
|
+
return useMemo(() => ({
|
|
605
611
|
hasAll,
|
|
606
612
|
isLoading,
|
|
607
613
|
error,
|
|
608
614
|
refetch: checkAllPermissions
|
|
609
|
-
};
|
|
615
|
+
}), [hasAll, isLoading, error, checkAllPermissions]);
|
|
610
616
|
}
|
|
611
617
|
|
|
612
618
|
/**
|
|
@@ -674,11 +680,12 @@ export function useCachedPermissions(userId: UUID, scope: Scope): {
|
|
|
674
680
|
fetchCachedPermissions();
|
|
675
681
|
}, [fetchCachedPermissions]);
|
|
676
682
|
|
|
677
|
-
return
|
|
683
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
684
|
+
return useMemo(() => ({
|
|
678
685
|
permissions,
|
|
679
686
|
isLoading,
|
|
680
687
|
error,
|
|
681
688
|
invalidateCache,
|
|
682
689
|
refetch: fetchCachedPermissions
|
|
683
|
-
};
|
|
690
|
+
}), [permissions, isLoading, error, invalidateCache, fetchCachedPermissions]);
|
|
684
691
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rbac/hooks/useRBAC.ts","../src/rbac/hooks/usePermissions.ts"],"sourcesContent":["/**\n * @file RBAC Hook\n * @package @jmruthers/pace-core\n * @module RBAC/Hooks\n * @since 0.3.0\n *\n * A React hook that provides access to the new RBAC (Role-Based Access Control) system.\n * This hook integrates with the database to provide real-time role and permission information.\n *\n * Features:\n * - Real-time role detection (global, organisation, event-app)\n * - Permission checking with database validation\n * - Hierarchical permission resolution\n * - Loading states and error handling\n * - Type-safe permission operations\n * - Automatic context detection\n *\n * @example\n * ```tsx\n * import { useRBAC } from '@jmruthers/pace-core/rbac';\n * \n * function MyComponent() {\n * const {\n * globalRole,\n * organisationRole,\n * eventAppRole,\n * hasPermission,\n * isSuperAdmin,\n * isLoading,\n * error\n * } = useRBAC();\n * \n * if (isLoading) return <div>Loading permissions...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return (\n * <div>\n * {isSuperAdmin && <AdminPanel />}\n * {hasPermission('read', 'dashboard') && <Dashboard />}\n * {hasPermission('create', 'events') && <CreateEventButton />}\n * </div>\n * );\n * }\n * ```\n *\n * @accessibility\n * - No direct accessibility concerns (hook)\n * - Enables accessible permission-based UI rendering\n * - Supports screen reader friendly conditional content\n *\n * @security\n * - Database-backed permission validation\n * - Hierarchical permission resolution\n * - Organisation context enforcement\n * - Real-time permission updates\n *\n * @performance\n * - Optimized with useMemo and useCallback\n * - Permission caching\n * - Minimal re-renders\n * - Lazy loading of permissions\n *\n * @dependencies\n * - React 18+ - Hooks and effects\n * - @supabase/supabase-js - Database integration\n * - RBAC types - Type definitions\n */\n\nimport { useState, useEffect, useCallback, useMemo } from 'react';\nimport { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';\nimport { useOrganisations } from '../../providers/OrganisationProvider';\nimport { useEvents } from '../../providers/EventProvider';\nimport type { \n UserRBACContext, \n GlobalRole, \n OrganisationRole, \n EventAppRole, \n Operation,\n RBACPermission \n} from '../types';\n\nexport function useRBAC(pageId?: string): UserRBACContext {\n const { user, session, supabase, appName } = useUnifiedAuth();\n const { selectedOrganisation } = useOrganisations();\n \n // Try to get events context, but don't fail if not available\n let selectedEvent = null;\n try {\n const eventsContext = useEvents();\n selectedEvent = eventsContext.selectedEvent;\n } catch (error) {\n // EventProvider not available, continue without event context\n console.debug('useRBAC: EventProvider not available, continuing without event context');\n }\n \n // State\n const [globalRole, setGlobalRole] = useState<GlobalRole | null>(null);\n const [organisationRole, setOrganisationRole] = useState<OrganisationRole | null>(null);\n const [eventAppRole, setEventAppRole] = useState<EventAppRole | null>(null);\n const [permissions, setPermissions] = useState<RBACPermission[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Load RBAC context\n const loadRBACContext = useCallback(async () => {\n if (!user || !session || !supabase || !appName) {\n console.log('[useRBAC] Missing required dependencies, clearing RBAC state');\n setGlobalRole(null);\n setOrganisationRole(null);\n setEventAppRole(null);\n setPermissions([]);\n return;\n }\n\n setIsLoading(true);\n setError(null);\n\n try {\n // First resolve app name to app_id\n const { data: appData, error: appError } = await supabase\n .from('rbac_apps')\n .select('id')\n .eq('name', appName)\n .eq('is_active', true)\n .single();\n\n if (appError || !appData) {\n console.warn('App not found or inactive:', appName);\n setIsLoading(false);\n return;\n }\n\n const { data, error: rpcError } = await supabase.rpc('get_rbac_permissions', {\n p_user_id: user.id,\n p_app_id: appData.id,\n p_event_id: selectedEvent?.event_id || null,\n p_organisation_id: selectedOrganisation?.id || null,\n p_page_id: pageId || null\n });\n\n if (rpcError) {\n throw new Error(`Failed to load RBAC permissions: ${rpcError.message}`);\n }\n\n if (data) {\n // Extract roles from permissions based on permission_type\n const globalPerms = data.filter((p: RBACPermission) => p.permission_type === 'all_permissions');\n const orgPerms = data.filter((p: RBACPermission) => p.permission_type === 'organisation_access');\n const eventPerms = data.filter((p: RBACPermission) => p.permission_type === 'event_app_access');\n\n // Set roles (take the highest permission for each type)\n setGlobalRole(globalPerms.length > 0 ? 'super_admin' : null);\n setOrganisationRole(orgPerms.length > 0 ? orgPerms[0].role_name as OrganisationRole : null);\n setEventAppRole(eventPerms.length > 0 ? eventPerms[0].role_name as EventAppRole : null);\n setPermissions(data);\n } else {\n setPermissions([]);\n }\n } catch (err) {\n console.error('RBAC Context Loading Error:', err);\n setError(err instanceof Error ? err : new Error('Unknown error loading RBAC context'));\n setPermissions([]);\n } finally {\n setIsLoading(false);\n }\n }, [user?.id, session, supabase, appName, selectedEvent?.event_id, selectedOrganisation?.id, pageId]);\n\n // Permission checking function\n const hasPermission = useCallback(async (operation: Operation, targetPageId?: string): Promise<boolean> => {\n if (!user || !session || !supabase || !appName) return false;\n\n // Super admin has all permissions\n if (globalRole === 'super_admin') {\n return true;\n }\n\n try {\n // First resolve app name to app_id\n const { data: appData, error: appError } = await supabase\n .from('rbac_apps')\n .select('id')\n .eq('name', appName)\n .eq('is_active', true)\n .single();\n\n if (appError || !appData) {\n console.warn('App not found or inactive:', appName);\n return false;\n }\n\n const { data, error } = await supabase.rpc('check_page_permission', {\n p_user_id: user.id,\n p_app_id: appData.id,\n p_page_id: targetPageId || pageId || 'default',\n p_operation: operation,\n p_event_id: selectedEvent?.event_id,\n p_organisation_id: selectedOrganisation?.id\n });\n\n if (error) {\n console.error('Permission check failed:', error);\n return false;\n }\n\n return data === true;\n } catch (err) {\n console.error('Permission check error:', err);\n return false;\n }\n }, [user, supabase, appName, selectedEvent, selectedOrganisation, pageId, globalRole]);\n\n // Simple permission check for global roles (synchronous)\n const hasGlobalPermission = useCallback((permission: string): boolean => {\n // Super admin has all permissions\n if (globalRole === 'super_admin') {\n return true;\n }\n \n // Check specific global roles\n if (permission === 'super_admin') {\n return globalRole === 'super_admin';\n }\n \n if (permission === 'org_admin') {\n return organisationRole === 'org_admin' || globalRole === 'super_admin';\n }\n \n return false;\n }, [globalRole, organisationRole]);\n\n // Computed properties\n const isSuperAdmin = useMemo(() => globalRole === 'super_admin', [globalRole]);\n const isOrgAdmin = useMemo(() => organisationRole === 'org_admin', [organisationRole]);\n const isEventAdmin = useMemo(() => eventAppRole === 'event_admin', [eventAppRole]);\n const canManageOrganisation = useMemo(() => \n isSuperAdmin || isOrgAdmin, [isSuperAdmin, isOrgAdmin]\n );\n const canManageEvent = useMemo(() => \n isSuperAdmin || isEventAdmin, [isSuperAdmin, isEventAdmin]\n );\n\n // Load context when dependencies change\n useEffect(() => {\n loadRBACContext();\n }, [loadRBACContext]);\n\n return {\n user, // Add user to the returned context\n globalRole,\n organisationRole,\n eventAppRole,\n hasPermission,\n hasGlobalPermission,\n isSuperAdmin,\n isOrgAdmin,\n isEventAdmin,\n canManageOrganisation,\n canManageEvent,\n isLoading,\n error\n };\n}\n","/**\n * @file RBAC Permission Hooks\n * @package @jmruthers/pace-core\n * @module RBAC/Hooks\n * @since 1.0.0\n * \n * This module provides React hooks for RBAC functionality.\n */\n\nimport { useState, useEffect, useCallback, useMemo, useRef } from 'react';\nimport { \n UUID, \n Scope, \n Permission, \n PermissionMap\n} from '../types';\nimport { AccessLevel as AccessLevelType } from '../types';\nimport { \n getAccessLevel, \n getPermissionMap, \n isPermitted,\n isPermittedCached \n} from '../api';\n\n/**\n * Hook to get user's permissions in a scope\n * \n * @param userId - User ID\n * @param scope - Scope for permission checking\n * @returns Permission state and methods\n * \n * @example\n * ```tsx\n * function MyComponent() {\n * const { permissions, isLoading, error } = usePermissions(userId, scope);\n * \n * if (isLoading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return (\n * <div>\n * {permissions['read:users'] && <UserList />}\n * {permissions['create:users'] && <CreateUserButton />}\n * </div>\n * );\n * }\n * ```\n */\nexport function usePermissions(userId: UUID, scope: Scope) {\n const [permissions, setPermissions] = useState<PermissionMap>({});\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const isFetchingRef = useRef(false);\n\n useEffect(() => {\n const fetchPermissions = async () => {\n // Prevent multiple simultaneous fetches\n if (isFetchingRef.current) {\n return;\n }\n\n if (!userId) {\n setPermissions({});\n setIsLoading(false);\n return;\n }\n\n // Don't fetch permissions if scope is invalid (e.g., organisationId is empty)\n // Return empty permissions and loading true for invalid scopes to prevent premature access denied\n if (!scope.organisationId || scope.organisationId.trim() === '') {\n setPermissions({});\n setIsLoading(true);\n setError(null);\n return;\n }\n\n try {\n isFetchingRef.current = true;\n setIsLoading(true);\n setError(null);\n \n const permissionMap = await getPermissionMap({ userId, scope });\n setPermissions(permissionMap);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));\n } finally {\n setIsLoading(false);\n isFetchingRef.current = false;\n }\n };\n\n fetchPermissions();\n }, [userId, scope.organisationId, scope.eventId, scope.appId]);\n\n const hasPermission = useCallback((permission: Permission): boolean => {\n return !!permissions[permission];\n }, [permissions]);\n\n const hasAnyPermission = useCallback((permissionList: Permission[]): boolean => {\n return permissionList.some(p => !!permissions[p]);\n }, [permissions]);\n\n const hasAllPermissions = useCallback((permissionList: Permission[]): boolean => {\n return permissionList.every(p => !!permissions[p]);\n }, [permissions]);\n\n const refetch = useCallback(async () => {\n // Prevent multiple simultaneous fetches\n if (isFetchingRef.current) {\n return;\n }\n\n if (!userId) {\n setPermissions({});\n setIsLoading(false);\n return;\n }\n\n // Don't fetch permissions if scope is invalid (e.g., organisationId is empty)\n if (!scope.organisationId || scope.organisationId.trim() === '') {\n setPermissions({});\n setIsLoading(true);\n setError(null);\n return;\n }\n\n try {\n isFetchingRef.current = true;\n setIsLoading(true);\n setError(null);\n \n const permissionMap = await getPermissionMap({ userId, scope });\n setPermissions(permissionMap);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));\n } finally {\n setIsLoading(false);\n isFetchingRef.current = false;\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId]);\n\n return {\n permissions,\n isLoading,\n error,\n hasPermission,\n hasAnyPermission,\n hasAllPermissions,\n refetch\n };\n}\n\n/**\n * Hook to check if user can perform an action\n * \n * @param userId - User ID\n * @param scope - Scope for permission checking\n * @param permission - Permission to check\n * @param pageId - Optional page ID\n * @param useCache - Whether to use cached results\n * @returns Permission check state and methods\n * \n * @example\n * ```tsx\n * function MyComponent() {\n * const { can, isLoading, error } = useCan(userId, scope, 'read:users');\n * \n * if (isLoading) return <div>Checking permission...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return can ? <UserList /> : <div>Access denied</div>;\n * }\n * ```\n */\nexport function useCan(\n userId: UUID, \n scope: Scope, \n permission: Permission, \n pageId?: UUID,\n useCache: boolean = true\n) {\n const [can, setCan] = useState<boolean>(false);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n // Use refs to track the last values to prevent unnecessary re-runs\n const lastUserIdRef = useRef<UUID | null>(null);\n const lastScopeRef = useRef<string | null>(null);\n const lastPermissionRef = useRef<Permission | null>(null);\n const lastPageIdRef = useRef<UUID | undefined | null>(null);\n const lastUseCacheRef = useRef<boolean | null>(null);\n\n useEffect(() => {\n // Create a scope key to track changes\n const scopeKey = `${scope.organisationId}-${scope.eventId}-${scope.appId}`;\n \n // Only run if something has actually changed\n if (\n lastUserIdRef.current !== userId ||\n lastScopeRef.current !== scopeKey ||\n lastPermissionRef.current !== permission ||\n lastPageIdRef.current !== pageId ||\n lastUseCacheRef.current !== useCache\n ) {\n lastUserIdRef.current = userId;\n lastScopeRef.current = scopeKey;\n lastPermissionRef.current = permission;\n lastPageIdRef.current = pageId;\n lastUseCacheRef.current = useCache;\n \n // Inline the permission check logic to avoid useCallback dependency issues\n const checkPermission = async () => {\n if (!userId) {\n setCan(false);\n setIsLoading(false);\n return;\n }\n\n // Don't check permissions if scope is invalid (e.g., organisationId is empty)\n // Return can: false and loading: true for invalid scopes to prevent premature access denied\n if (!scope.organisationId || scope.organisationId.trim() === '') {\n setCan(false);\n setIsLoading(true);\n return;\n }\n\n try {\n setIsLoading(true);\n setError(null);\n \n const result = useCache \n ? await isPermittedCached({ userId, scope, permission, pageId })\n : await isPermitted({ userId, scope, permission, pageId });\n \n setCan(result);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to check permission'));\n setCan(false);\n } finally {\n setIsLoading(false);\n }\n };\n \n checkPermission();\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId, permission, pageId, useCache]);\n\n const refetch = useCallback(async () => {\n if (!userId) {\n setCan(false);\n setIsLoading(false);\n return;\n }\n\n // Don't check permissions if scope is invalid (e.g., organisationId is empty)\n if (!scope.organisationId || scope.organisationId.trim() === '') {\n setCan(false);\n setIsLoading(true);\n return;\n }\n\n try {\n setIsLoading(true);\n setError(null);\n \n const result = useCache \n ? await isPermittedCached({ userId, scope, permission, pageId })\n : await isPermitted({ userId, scope, permission, pageId });\n \n setCan(result);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to check permission'));\n setCan(false);\n } finally {\n setIsLoading(false);\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId, permission, pageId, useCache]);\n\n return {\n can,\n isLoading,\n error,\n refetch\n };\n}\n\n/**\n * Hook to get user's access level in a scope\n * \n * @param userId - User ID\n * @param scope - Scope for access level checking\n * @returns Access level state and methods\n * \n * @example\n * ```tsx\n * function MyComponent() {\n * const { accessLevel, isLoading, error } = useAccessLevel(userId, scope);\n * \n * if (isLoading) return <div>Loading access level...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return (\n * <div>\n * Access Level: {accessLevel}\n * {accessLevel >= AccessLevel.ADMIN && <AdminPanel />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAccessLevel(userId: UUID, scope: Scope): {\n accessLevel: AccessLevelType;\n isLoading: boolean;\n error: Error | null;\n refetch: () => Promise<void>;\n} {\n const [accessLevel, setAccessLevel] = useState<AccessLevelType>('viewer');\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const fetchAccessLevel = useCallback(async () => {\n if (!userId) {\n setAccessLevel('viewer');\n setIsLoading(false);\n return;\n }\n\n try {\n setIsLoading(true);\n setError(null);\n \n const level = await getAccessLevel({ userId, scope });\n setAccessLevel(level);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch access level'));\n setAccessLevel('viewer');\n } finally {\n setIsLoading(false);\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId]);\n\n useEffect(() => {\n fetchAccessLevel();\n }, [fetchAccessLevel]);\n\n return {\n accessLevel,\n isLoading,\n error,\n refetch: fetchAccessLevel\n };\n}\n\n/**\n * Hook to check multiple permissions at once\n * \n * @param userId - User ID\n * @param scope - Scope for permission checking\n * @param permissions - Array of permissions to check\n * @param useCache - Whether to use cached results\n * @returns Multiple permission check results\n * \n * @example\n * ```tsx\n * function MyComponent() {\n * const { results, isLoading, error } = useMultiplePermissions(\n * userId, \n * scope, \n * ['read:users', 'create:users', 'update:users']\n * );\n * \n * if (isLoading) return <div>Checking permissions...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return (\n * <div>\n * {results['read:users'] && <UserList />}\n * {results['create:users'] && <CreateUserButton />}\n * {results['update:users'] && <EditUserButton />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useMultiplePermissions(\n userId: UUID, \n scope: Scope, \n permissions: Permission[],\n useCache: boolean = true\n): {\n results: Record<Permission, boolean>;\n isLoading: boolean;\n error: Error | null;\n refetch: () => Promise<void>;\n} {\n const [results, setResults] = useState<Record<Permission, boolean>>({} as Record<Permission, boolean>);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const checkPermissions = useCallback(async () => {\n if (!userId || permissions.length === 0) {\n setResults({} as Record<Permission, boolean>);\n setIsLoading(false);\n return;\n }\n\n try {\n setIsLoading(true);\n setError(null);\n \n const permissionResults: Record<Permission, boolean> = {} as Record<Permission, boolean>;\n \n // Check each permission\n for (const permission of permissions) {\n const result = useCache \n ? await isPermittedCached({ userId, scope, permission })\n : await isPermitted({ userId, scope, permission });\n permissionResults[permission] = result;\n }\n \n setResults(permissionResults);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to check permissions'));\n setResults({} as Record<Permission, boolean>);\n } finally {\n setIsLoading(false);\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);\n\n useEffect(() => {\n checkPermissions();\n }, [checkPermissions]);\n\n return {\n results,\n isLoading,\n error,\n refetch: checkPermissions\n };\n}\n\n/**\n * Hook to check if user has any of the specified permissions\n * \n * @param userId - User ID\n * @param scope - Scope for permission checking\n * @param permissions - Array of permissions to check\n * @param useCache - Whether to use cached results\n * @returns Whether user has any of the permissions\n * \n * @example\n * ```tsx\n * function MyComponent() {\n * const { hasAny, isLoading, error } = useHasAnyPermission(\n * userId, \n * scope, \n * ['read:users', 'create:users']\n * );\n * \n * if (isLoading) return <div>Checking permissions...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return hasAny ? <UserManagementPanel /> : <div>No user permissions</div>;\n * }\n * ```\n */\nexport function useHasAnyPermission(\n userId: UUID, \n scope: Scope, \n permissions: Permission[],\n useCache: boolean = true\n): {\n hasAny: boolean;\n isLoading: boolean;\n error: Error | null;\n refetch: () => Promise<void>;\n} {\n const [hasAny, setHasAny] = useState<boolean>(false);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const checkAnyPermission = useCallback(async () => {\n if (!userId || permissions.length === 0) {\n setHasAny(false);\n setIsLoading(false);\n return;\n }\n\n try {\n setIsLoading(true);\n setError(null);\n \n let hasAnyPermission = false;\n \n for (const permission of permissions) {\n const result = useCache \n ? await isPermittedCached({ userId, scope, permission })\n : await isPermitted({ userId, scope, permission });\n \n if (result) {\n hasAnyPermission = true;\n break;\n }\n }\n \n setHasAny(hasAnyPermission);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to check permissions'));\n setHasAny(false);\n } finally {\n setIsLoading(false);\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);\n\n useEffect(() => {\n checkAnyPermission();\n }, [checkAnyPermission]);\n\n return {\n hasAny,\n isLoading,\n error,\n refetch: checkAnyPermission\n };\n}\n\n/**\n * Hook to check if user has all of the specified permissions\n * \n * @param userId - User ID\n * @param scope - Scope for permission checking\n * @param permissions - Array of permissions to check\n * @param useCache - Whether to use cached results\n * @returns Whether user has all of the permissions\n * \n * @example\n * ```tsx\n * function MyComponent() {\n * const { hasAll, isLoading, error } = useHasAllPermissions(\n * userId, \n * scope, \n * ['read:users', 'create:users', 'update:users']\n * );\n * \n * if (isLoading) return <div>Checking permissions...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return hasAll ? <FullUserManagementPanel /> : <div>Insufficient permissions</div>;\n * }\n * ```\n */\nexport function useHasAllPermissions(\n userId: UUID, \n scope: Scope, \n permissions: Permission[],\n useCache: boolean = true\n): {\n hasAll: boolean;\n isLoading: boolean;\n error: Error | null;\n refetch: () => Promise<void>;\n} {\n const [hasAll, setHasAll] = useState<boolean>(false);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const checkAllPermissions = useCallback(async () => {\n if (!userId || permissions.length === 0) {\n setHasAll(false);\n setIsLoading(false);\n return;\n }\n\n try {\n setIsLoading(true);\n setError(null);\n \n let hasAllPermissions = true;\n \n for (const permission of permissions) {\n const result = useCache \n ? await isPermittedCached({ userId, scope, permission })\n : await isPermitted({ userId, scope, permission });\n \n if (!result) {\n hasAllPermissions = false;\n break;\n }\n }\n \n setHasAll(hasAllPermissions);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to check permissions'));\n setHasAll(false);\n } finally {\n setIsLoading(false);\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, useCache]);\n\n useEffect(() => {\n checkAllPermissions();\n }, [checkAllPermissions]);\n\n return {\n hasAll,\n isLoading,\n error,\n refetch: checkAllPermissions\n };\n}\n\n/**\n * Hook to get cached permissions with TTL management\n * \n * @param userId - User ID\n * @param scope - Scope for permission checking\n * @returns Cached permission state and methods\n * \n * @example\n * ```tsx\n * function MyComponent() {\n * const { permissions, isLoading, error, invalidateCache } = useCachedPermissions(userId, scope);\n * \n * if (isLoading) return <div>Loading cached permissions...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n * \n * return (\n * <div>\n * {permissions['read:users'] && <UserList />}\n * <button onClick={invalidateCache}>Refresh Permissions</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useCachedPermissions(userId: UUID, scope: Scope): {\n permissions: PermissionMap;\n isLoading: boolean;\n error: Error | null;\n invalidateCache: () => void;\n refetch: () => Promise<void>;\n} {\n const [permissions, setPermissions] = useState<PermissionMap>({});\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const fetchCachedPermissions = useCallback(async () => {\n if (!userId) {\n setPermissions({});\n setIsLoading(false);\n return;\n }\n\n try {\n setIsLoading(true);\n setError(null);\n \n const permissionMap = await getPermissionMap({ userId, scope });\n setPermissions(permissionMap);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch cached permissions'));\n } finally {\n setIsLoading(false);\n }\n }, [userId, scope.organisationId, scope.eventId, scope.appId]);\n\n const invalidateCache = useCallback(() => {\n // This would typically invalidate the cache in the actual implementation\n // For now, we'll just refetch\n fetchCachedPermissions();\n }, [fetchCachedPermissions]);\n\n useEffect(() => {\n fetchCachedPermissions();\n }, [fetchCachedPermissions]);\n\n return {\n permissions,\n isLoading,\n error,\n invalidateCache,\n refetch: fetchCachedPermissions\n };\n}\n"],"mappings":"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","names":["error","useState","useEffect","useCallback","useState","useEffect","useCallback"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/appIdResolver.ts","../src/providers/EventProvider.tsx"],"sourcesContent":["/**\n * App ID Resolution Utility\n * @package @jmruthers/pace-core\n * @module Utils/AppIdResolver\n * @since 1.0.0\n * \n * This module provides utilities to resolve app names to app IDs for database operations.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { Database } from '../types/database';\n\n/**\n * Resolves an app name to its corresponding app ID\n * \n * @param supabase - Supabase client instance\n * @param appName - The app name to resolve\n * @returns Promise resolving to the app ID or null if not found\n */\nexport async function getAppId(\n supabase: SupabaseClient<Database>,\n appName: string\n): Promise<string | null> {\n try {\n const { data, error } = await supabase\n .from('rbac_apps')\n .select('id')\n .ilike('name', appName)\n .eq('is_active', true)\n .single();\n\n if (error) {\n console.error('Failed to resolve app ID for app name:', appName, error);\n return null;\n }\n\n return (data as { id: string } | null)?.id || null;\n } catch (error) {\n console.error('Error resolving app ID for app name:', appName, error);\n return null;\n }\n}\n\n/**\n * Resolves multiple app names to their corresponding app IDs\n * \n * @param supabase - Supabase client instance\n * @param appNames - Array of app names to resolve\n * @returns Promise resolving to a map of app names to app IDs\n */\nexport async function getAppIds(\n supabase: SupabaseClient<Database>,\n appNames: string[]\n): Promise<Record<string, string | null>> {\n try {\n // For case-insensitive matching with multiple values, we need to use OR conditions\n // since PostgreSQL doesn't support case-insensitive IN with ILIKE\n const orConditions = appNames.map(name => `name.ilike.${name}`).join(',');\n \n const { data, error } = await supabase\n .from('rbac_apps')\n .select('id, name')\n .or(orConditions)\n .eq('is_active', true);\n\n if (error) {\n console.error('Failed to resolve app IDs for app names:', appNames, error);\n return {};\n }\n\n const result: Record<string, string | null> = {};\n \n // Initialize all app names with null\n appNames.forEach(name => {\n result[name] = null;\n });\n\n // Set resolved app IDs - match case-insensitively\n (data as { id: string; name: string }[] | null)?.forEach(app => {\n // Find the original app name that matches (case-insensitive)\n const originalName = appNames.find(name => \n name.toLowerCase() === app.name.toLowerCase()\n );\n if (originalName) {\n result[originalName] = app.id;\n }\n });\n\n return result;\n } catch (error) {\n console.error('Error resolving app IDs for app names:', appNames, error);\n return {};\n }\n}\n\n/**\n * Cached app ID resolver with TTL\n */\nclass CachedAppIdResolver {\n private cache = new Map<string, { id: string | null; expires: number }>();\n private ttl = 5 * 60 * 1000; // 5 minutes\n\n async getAppId(\n supabase: SupabaseClient<Database>,\n appName: string\n ): Promise<string | null> {\n const now = Date.now();\n const cached = this.cache.get(appName);\n\n if (cached && cached.expires > now) {\n return cached.id;\n }\n\n const id = await getAppId(supabase, appName);\n this.cache.set(appName, { id, expires: now + this.ttl });\n\n return id;\n }\n\n clearCache(): void {\n this.cache.clear();\n }\n\n clearCacheForApp(appName: string): void {\n this.cache.delete(appName);\n }\n}\n\n// Export singleton instance\nexport const cachedAppIdResolver = new CachedAppIdResolver();\n","import React, {\n createContext,\n useContext,\n useState,\n useEffect,\n useLayoutEffect,\n useCallback,\n useRef,\n} from 'react';\nimport { useUnifiedAuth } from './UnifiedAuthProvider';\nimport { useOrganisations } from './OrganisationProvider';\nimport { setOrganisationContext } from '../utils/organisationContext';\nimport { DebugLogger } from '../utils/debugLogger';\nimport { cachedAppIdResolver } from '../utils/appIdResolver';\n\nimport { Event } from '../types/unified';\n\nexport interface EventContextType {\n events: Event[];\n selectedEvent: Event | null;\n isLoading: boolean;\n error: Error | null;\n setSelectedEvent: (event: Event | null) => void;\n refreshEvents: () => Promise<void>;\n}\n\nconst EventContext = createContext<EventContextType | undefined>(undefined);\n\nexport const useEvents = () => {\n const context = useContext(EventContext);\n if (context === undefined) {\n throw new Error('useEvents must be used within an EventProvider');\n }\n return context;\n};\n\nexport interface EventProviderProps {\n children: React.ReactNode;\n}\n\n// Helper function to get the next event by date\nfunction getNextEventByDate(events: Event[]): Event | null {\n if (!events || events.length === 0) {\n return null;\n }\n\n const now = new Date();\n const futureEvents = events.filter(event => {\n if (!event.event_date) return false;\n const eventDate = new Date(event.event_date);\n return eventDate >= now;\n });\n\n if (futureEvents.length === 0) {\n return null;\n }\n\n // Sort by date (ascending) to get the next event\n const sortedFutureEvents = futureEvents.sort((a, b) => {\n const dateA = new Date(a.event_date!);\n const dateB = new Date(b.event_date!);\n return dateA.getTime() - dateB.getTime();\n });\n\n return sortedFutureEvents[0];\n}\n\nexport function EventProvider({ children }: EventProviderProps) {\n const [events, setEvents] = useState<Event[]>([]);\n const [selectedEvent, setSelectedEventState] = useState<Event | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const { user, session, supabase, appName, setSelectedEventId } = useUnifiedAuth();\n \n // Refs to prevent infinite loops\n const isInitializedRef = useRef(false);\n const isFetchingRef = useRef(false);\n const hasAutoSelectedRef = useRef(false);\n const userClearedEventRef = useRef(false);\n const setSelectedEventIdRef = useRef(setSelectedEventId);\n \n // Try to get organisation context, but don't fail if not available\n let selectedOrganisation = null;\n let ensureOrganisationContext = null;\n \n try {\n const orgContext = useOrganisations();\n selectedOrganisation = orgContext.selectedOrganisation;\n ensureOrganisationContext = orgContext.ensureOrganisationContext;\n } catch (error) {\n console.warn('[EventProvider] Organisation context not available:', error);\n }\n\n // Load persisted event selection with tab-scoped storage\n const loadPersistedEvent = useCallback(async (events: Event[]) => {\n try {\n // Try sessionStorage first (tab-specific)\n let persistedEventId = sessionStorage.getItem('pace-core-selected-event');\n \n // Fallback to localStorage if no sessionStorage value (for new tabs)\n if (!persistedEventId) {\n persistedEventId = localStorage.getItem('pace-core-selected-event');\n // If we found a value in localStorage, also store it in sessionStorage for this tab\n if (persistedEventId) {\n sessionStorage.setItem('pace-core-selected-event', persistedEventId);\n }\n }\n \n if (persistedEventId && events.length > 0) {\n const persistedEvent = events.find(event => event.event_id === persistedEventId);\n if (persistedEvent) {\n DebugLogger.log('EventProvider', 'Restoring persisted event:', persistedEvent.event_name);\n setSelectedEventState(persistedEvent);\n setSelectedEventId(persistedEventId);\n return true;\n }\n }\n } catch (error) {\n console.warn('[EventProvider] Failed to load persisted event:', error);\n }\n return false;\n }, [setSelectedEventId]);\n\n // Persist event selection with tab-scoped storage\n const persistEventSelection = useCallback((eventId: string) => {\n try {\n // Store in sessionStorage for tab-specific persistence\n sessionStorage.setItem('pace-core-selected-event', eventId);\n // Also store in localStorage as fallback for new tabs\n localStorage.setItem('pace-core-selected-event', eventId);\n } catch (error) {\n console.warn('[EventProvider] Failed to persist event selection:', error);\n }\n }, []);\n\n // Auto-select next event\n const autoSelectNextEvent = useCallback((events: Event[]) => {\n const nextEvent = getNextEventByDate(events);\n if (nextEvent) {\n DebugLogger.log('EventProvider', 'Auto-selecting next event:', nextEvent.event_name);\n setSelectedEventState(nextEvent);\n setSelectedEventId(nextEvent.event_id);\n persistEventSelection(nextEvent.event_id);\n }\n }, [setSelectedEventId, persistEventSelection]);\n\n // Main fetch function\n const fetchEvents = useCallback(async () => {\n if (!user || !session || !supabase || !appName || !selectedOrganisation) {\n DebugLogger.log('EventProvider', 'Missing required dependencies, skipping fetch');\n setIsLoading(false);\n return;\n }\n\n // Prevent multiple simultaneous fetches\n if (isFetchingRef.current) {\n DebugLogger.log('EventProvider', 'Already fetching events, skipping');\n return;\n }\n\n DebugLogger.log('EventProvider', 'User and organisation found, fetching events for:', {\n userId: user.id,\n appName: appName,\n organisationId: selectedOrganisation.id,\n organisationName: selectedOrganisation.display_name\n });\n\n isFetchingRef.current = true;\n let isMounted = true;\n\n try {\n // Ensure organisation context is set\n if (ensureOrganisationContext) {\n await ensureOrganisationContext();\n await setOrganisationContext(supabase, selectedOrganisation.id);\n }\n\n // Resolve app name to app ID\n const appId = await cachedAppIdResolver.getAppId(supabase, appName);\n \n if (!appId) {\n throw new Error(`App ID not found for app name: ${appName}`);\n }\n\n // Call the RPC function with app_id\n DebugLogger.log('EventProvider', 'Calling get_pace_user_events RPC with:', {\n user_uuid: user.id,\n app_id: appId,\n p_organisation_id: selectedOrganisation.id\n });\n\n const response = await supabase.rpc('get_pace_user_events', {\n user_uuid: user.id,\n app_id: appId,\n p_organisation_id: selectedOrganisation.id,\n });\n \n const { data, error: rpcError } = response || {};\n\n DebugLogger.log('EventProvider', 'RPC response:', {\n data,\n error: rpcError,\n dataLength: data?.length || 0,\n organisationId: selectedOrganisation.id\n });\n\n if (rpcError) {\n throw new Error(rpcError.message || 'Failed to fetch events');\n }\n\n if (isMounted) {\n const eventsData = data || [];\n setEvents(eventsData);\n setError(null);\n\n // Note: Event colors are now managed by useEventTheme hook based on route and selected event\n // This prevents unwanted color cycling on non-event pages\n\n // Reset auto-selection ref for new events\n hasAutoSelectedRef.current = false;\n\n // Try to restore persisted event first\n await loadPersistedEvent(eventsData);\n }\n } catch (err) {\n console.error('[EventProvider] Error fetching events:', err);\n const _error = err instanceof Error ? err : new Error('Unknown error occurred');\n \n if (isMounted) {\n setError(_error);\n setEvents([]);\n }\n } finally {\n if (isMounted) {\n setIsLoading(false);\n }\n isFetchingRef.current = false;\n }\n\n return () => {\n isMounted = false;\n };\n }, [user, session, supabase, appName, selectedOrganisation, ensureOrganisationContext, loadPersistedEvent, autoSelectNextEvent]);\n\n // Initialize events only once\n useEffect(() => {\n if (!isInitializedRef.current) {\n isInitializedRef.current = true;\n fetchEvents();\n }\n }, [fetchEvents]);\n\n // Update ref when setSelectedEventId changes\n useEffect(() => {\n setSelectedEventIdRef.current = setSelectedEventId;\n }, [setSelectedEventId]);\n\n // Auto-select next event when events are loaded and no event is selected\n useLayoutEffect(() => {\n if (events.length > 0 && !selectedEvent && !hasAutoSelectedRef.current && !userClearedEventRef.current) {\n const nextEvent = getNextEventByDate(events);\n if (nextEvent) {\n DebugLogger.log('EventProvider', 'Auto-selecting next event:', nextEvent.event_name);\n hasAutoSelectedRef.current = true;\n setSelectedEventState(nextEvent);\n setSelectedEventIdRef.current(nextEvent.event_id);\n persistEventSelection(nextEvent.event_id);\n }\n }\n }, [events, selectedEvent]);\n\n const setSelectedEvent = useCallback((event: Event | null) => {\n if (event) {\n // SECURITY: Validate event belongs to current organisation\n try {\n if (selectedOrganisation && event.organisation_id !== selectedOrganisation.id) {\n console.error('[EventProvider] Event organisation_id does not match selected organisation');\n return;\n }\n } catch (error) {\n // Silently handle validation errors\n }\n\n setSelectedEventState(event);\n setSelectedEventId(event.event_id);\n persistEventSelection(event.event_id);\n // Reset the user cleared flag when selecting an event\n userClearedEventRef.current = false;\n } else {\n setSelectedEventState(null);\n setSelectedEventId(null);\n // Clear both sessionStorage and localStorage\n sessionStorage.removeItem('pace-core-selected-event');\n localStorage.removeItem('pace-core-selected-event');\n // Reset the auto-selection flag when clearing the event\n hasAutoSelectedRef.current = false;\n // Mark that user explicitly cleared the event to prevent auto-selection\n userClearedEventRef.current = true;\n }\n }, [selectedOrganisation, setSelectedEventId, persistEventSelection]);\n\n const refreshEvents = useCallback(async () => {\n isInitializedRef.current = false;\n isFetchingRef.current = false;\n // Reset the user cleared flag when refreshing events\n userClearedEventRef.current = false;\n await fetchEvents();\n }, [fetchEvents]);\n\n const contextValue: EventContextType = {\n events,\n selectedEvent,\n isLoading,\n error,\n setSelectedEvent,\n refreshEvents,\n };\n\n return (\n <EventContext.Provider value={contextValue}>\n {children}\n </EventContext.Provider>\n );\n}"],"mappings":"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","names":["error","events"]}
|