@jmruthers/pace-core 0.6.7 → 0.6.8
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/audit-tool/00-dependencies.cjs +215 -9
- package/audit-tool/audits/02-project-structure.cjs +3 -18
- package/audit-tool/audits/03-architecture.cjs +34 -6
- package/audit-tool/audits/06-security-rbac.cjs +10 -0
- package/audit-tool/audits/07-api-tech-stack.cjs +55 -1
- package/audit-tool/index.cjs +23 -19
- package/audit-tool/utils/report-utils.cjs +141 -2
- package/dist/{DataTable-7PMH7XN7.js → DataTable-6RMSCQJ6.js} +5 -5
- package/dist/{PublicPageProvider-DlsCaR5v.d.ts → PublicPageProvider-CIGSujI2.d.ts} +14 -8
- package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
- package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
- package/dist/{chunk-L4XMVJKY.js → chunk-4DDCYDQ3.js} +8 -7
- package/dist/{chunk-ZKAWKYT4.js → chunk-5W2A3DRC.js} +2 -1
- package/dist/{chunk-VBCS3DUA.js → chunk-EF2UGZWY.js} +3 -3
- package/dist/{chunk-JGWDVX64.js → chunk-EURB7QFZ.js} +123 -53
- package/dist/{chunk-BM4CQ5P3.js → chunk-GS5672WG.js} +6 -6
- package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
- package/dist/{chunk-5X4QLXRG.js → chunk-MPBLMWVR.js} +5 -3
- package/dist/{chunk-Q7Q7V5NV.js → chunk-NKHKXPI4.js} +7 -7
- package/dist/{chunk-6F3IILHI.js → chunk-S6ZQKDY6.js} +1 -1
- package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
- package/dist/{chunk-GHYHJTYV.js → chunk-Z2FNRKF3.js} +13 -13
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -12
- package/dist/eslint-rules/rules/04-code-quality.cjs +66 -10
- package/dist/eslint-rules/rules/06-security-rbac.cjs +8 -3
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +190 -68
- package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -15
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +6 -6
- package/dist/theming/runtime.d.ts +48 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/utils.js +1 -1
- package/docs/api/modules.md +63 -14
- package/docs/getting-started/dependencies.md +23 -0
- package/docs/implementation-guides/app-layout.md +1 -1
- package/docs/implementation-guides/data-tables.md +1 -1
- package/docs/standards/1-pace-core-compliance-standards.md +38 -1
- package/eslint-config-pace-core.cjs +30 -11
- package/package.json +45 -15
- package/scripts/eslint-audit.cjs +123 -0
- package/scripts/install-eslint-config.cjs +67 -2
- package/scripts/validate-dependencies.cjs +248 -0
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +20 -8
- package/src/__tests__/templates/accessibility.test.template.tsx +1 -0
- package/src/components/AddressField/AddressField.tsx +26 -1
- package/src/components/Alert/Alert.test.tsx +86 -22
- package/src/components/Alert/Alert.tsx +19 -11
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Checkbox/Checkbox.test.tsx +2 -1
- package/src/components/ContextSelector/ContextSelector.tsx +39 -41
- package/src/components/DataTable/DataTable.tsx +1 -19
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -10
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +18 -9
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
- package/src/components/DataTable/components/EmptyState.tsx +1 -1
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +1 -1
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +3 -3
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +33 -29
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -2
- package/src/components/FileUpload/FileUpload.test.tsx +22 -31
- package/src/components/FileUpload/FileUpload.tsx +29 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +3 -3
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
- package/src/hooks/public/usePublicRouteParams.ts +8 -4
- package/src/hooks/useAddressAutocomplete.test.ts +18 -18
- package/src/hooks/useEventTheme.ts +5 -1
- package/src/hooks/useFileUrl.ts +52 -8
- package/src/hooks/useOrganisationSecurity.test.ts +2 -1
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +1 -1
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
- package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
- package/src/rbac/api.test.ts +104 -0
- package/src/rbac/engine.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +2 -2
- package/src/rbac/secureClient.ts +1 -1
- package/src/rbac/types/functions.ts +1 -1
- package/src/theming/__tests__/parseEventColours.test.ts +117 -8
- package/src/theming/parseEventColours.ts +56 -2
- package/src/types/supabase.ts +2 -3
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
- package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
- package/src/utils/formatting/formatDate.test.ts +3 -2
- package/src/utils/formatting/formatDateTime.test.ts +2 -2
- package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
- package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
- package/src/utils/storage/helpers.test.ts +69 -3
package/src/hooks/useFileUrl.ts
CHANGED
|
@@ -57,12 +57,21 @@ export function useFileUrl(
|
|
|
57
57
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
58
58
|
const [error, setError] = useState<Error | null>(null);
|
|
59
59
|
const fileReferenceIdRef = useRef<string | null>(null);
|
|
60
|
+
const isLoadingRef = useRef<boolean>(false);
|
|
61
|
+
const urlRef = useRef<string | null>(null);
|
|
62
|
+
|
|
63
|
+
// Keep refs in sync with state
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
isLoadingRef.current = isLoading;
|
|
66
|
+
urlRef.current = url;
|
|
67
|
+
}, [isLoading, url]);
|
|
60
68
|
|
|
61
69
|
const loadUrl = useCallback(async () => {
|
|
62
70
|
if (!fileReference) {
|
|
63
71
|
setUrl(null);
|
|
64
72
|
setIsLoading(false);
|
|
65
73
|
setError(null);
|
|
74
|
+
fileReferenceIdRef.current = null;
|
|
66
75
|
return;
|
|
67
76
|
}
|
|
68
77
|
|
|
@@ -70,11 +79,12 @@ export function useFileUrl(
|
|
|
70
79
|
setUrl(null);
|
|
71
80
|
setIsLoading(false);
|
|
72
81
|
setError(new Error('Supabase client is required for URL generation'));
|
|
82
|
+
fileReferenceIdRef.current = null;
|
|
73
83
|
return;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
// Skip if already loading or URL already exists for this file
|
|
77
|
-
if (
|
|
87
|
+
if (isLoadingRef.current || (urlRef.current && fileReferenceIdRef.current === fileReference.id)) {
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -108,29 +118,63 @@ export function useFileUrl(
|
|
|
108
118
|
} finally {
|
|
109
119
|
setIsLoading(false);
|
|
110
120
|
}
|
|
111
|
-
}, [fileReference, supabase, organisation_id
|
|
121
|
+
}, [fileReference, supabase, organisation_id]);
|
|
112
122
|
|
|
113
123
|
const clear = useCallback(() => {
|
|
114
124
|
setUrl(null);
|
|
115
125
|
setError(null);
|
|
116
126
|
setIsLoading(false);
|
|
117
127
|
fileReferenceIdRef.current = null;
|
|
128
|
+
urlRef.current = null;
|
|
118
129
|
}, []);
|
|
119
130
|
|
|
120
131
|
// Auto-load URL when fileReference changes
|
|
121
132
|
useEffect(() => {
|
|
122
|
-
if (autoLoad) {
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
if (!autoLoad) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const currentFileId = fileReference?.id ?? null;
|
|
138
|
+
const previousFileId = fileReferenceIdRef.current;
|
|
139
|
+
|
|
140
|
+
// Handle null file reference
|
|
141
|
+
if (!fileReference) {
|
|
142
|
+
if (previousFileId !== null) {
|
|
125
143
|
setUrl(null);
|
|
126
144
|
setError(null);
|
|
145
|
+
fileReferenceIdRef.current = null;
|
|
146
|
+
urlRef.current = null;
|
|
127
147
|
}
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Reset URL when file reference ID changes (but not on initial mount)
|
|
152
|
+
if (previousFileId !== null && previousFileId !== currentFileId) {
|
|
153
|
+
setUrl(null);
|
|
154
|
+
setError(null);
|
|
155
|
+
fileReferenceIdRef.current = null;
|
|
156
|
+
urlRef.current = null; // Also reset the ref immediately
|
|
157
|
+
// Immediately try to load - loadUrl will check if already loading
|
|
158
|
+
loadUrl();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Initial load or reload after clear: Load URL if we have a file reference and no URL is set
|
|
163
|
+
if (previousFileId === null && currentFileId !== null) {
|
|
164
|
+
// First time loading this file or reloading after clear - update ref and load
|
|
165
|
+
fileReferenceIdRef.current = currentFileId;
|
|
166
|
+
if (!isLoading && !url) {
|
|
130
167
|
loadUrl();
|
|
131
168
|
}
|
|
169
|
+
} else if (previousFileId === currentFileId && !url && !isLoading) {
|
|
170
|
+
// Same file but URL not loaded yet (e.g., after clear or initial mount edge case)
|
|
171
|
+
// Ensure ref is set correctly
|
|
172
|
+
if (fileReferenceIdRef.current !== currentFileId) {
|
|
173
|
+
fileReferenceIdRef.current = currentFileId;
|
|
174
|
+
}
|
|
175
|
+
loadUrl();
|
|
132
176
|
}
|
|
133
|
-
}, [fileReference
|
|
177
|
+
}, [fileReference, autoLoad, loadUrl, url, isLoading]);
|
|
134
178
|
|
|
135
179
|
return {
|
|
136
180
|
url,
|
|
@@ -625,7 +625,8 @@ describe('useOrganisationSecurity', () => {
|
|
|
625
625
|
|
|
626
626
|
it('handles permission retrieval errors gracefully', async () => {
|
|
627
627
|
mockIsSuperAdmin.mockResolvedValue(false);
|
|
628
|
-
|
|
628
|
+
// getUserPermissions uses getPermissionMap, not isPermittedCached
|
|
629
|
+
mockGetPermissionMap.mockRejectedValue(new Error('Permission retrieval failed'));
|
|
629
630
|
|
|
630
631
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
631
632
|
|
|
@@ -82,7 +82,7 @@ describe('Provider Lifecycle Tests', () => {
|
|
|
82
82
|
|
|
83
83
|
rerender(<TestComponent isVisible={false} />);
|
|
84
84
|
|
|
85
|
-
expect(screen.queryByTestId('cleanup-test')).
|
|
85
|
+
expect(screen.queryByTestId('cleanup-test')).toBeNull();
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('should cleanup subscription listeners', () => {
|
|
@@ -109,9 +109,12 @@ describe('Auth/RBAC E2E Flows', () => {
|
|
|
109
109
|
}
|
|
110
110
|
return Promise.resolve({ data: false, error: null });
|
|
111
111
|
},
|
|
112
|
-
|
|
112
|
+
data_app_resolve: (params?: any) => {
|
|
113
113
|
// Return app ID for TEST_APP
|
|
114
|
-
return Promise.resolve({
|
|
114
|
+
return Promise.resolve({
|
|
115
|
+
data: [{ app_id: 'app-123', app_name: 'TEST_APP', is_active: true, has_access: true }],
|
|
116
|
+
error: null
|
|
117
|
+
});
|
|
115
118
|
},
|
|
116
119
|
data_user_organisation_roles_get: (params?: any) => {
|
|
117
120
|
// Return organisation roles/memberships
|
|
@@ -241,8 +244,11 @@ describe('Auth/RBAC E2E Flows', () => {
|
|
|
241
244
|
return Promise.resolve({ data: true, error: null });
|
|
242
245
|
}
|
|
243
246
|
// Handle other RPCs
|
|
244
|
-
if (functionName === '
|
|
245
|
-
return Promise.resolve({
|
|
247
|
+
if (functionName === 'data_app_resolve') {
|
|
248
|
+
return Promise.resolve({
|
|
249
|
+
data: [{ app_id: 'app-123', app_name: 'TEST_APP', is_active: true, has_access: true }],
|
|
250
|
+
error: null
|
|
251
|
+
});
|
|
246
252
|
}
|
|
247
253
|
if (functionName === 'data_user_organisation_roles_get') {
|
|
248
254
|
return Promise.resolve({ data: [], error: null });
|
|
@@ -419,8 +425,11 @@ describe('Auth/RBAC E2E Flows', () => {
|
|
|
419
425
|
return Promise.resolve({ data: false, error: null });
|
|
420
426
|
}
|
|
421
427
|
// Handle other RPCs
|
|
422
|
-
if (functionName === '
|
|
423
|
-
return Promise.resolve({
|
|
428
|
+
if (functionName === 'data_app_resolve') {
|
|
429
|
+
return Promise.resolve({
|
|
430
|
+
data: [{ app_id: 'app-123', app_name: 'TEST_APP', is_active: true, has_access: true }],
|
|
431
|
+
error: null
|
|
432
|
+
});
|
|
424
433
|
}
|
|
425
434
|
if (functionName === 'data_user_organisation_roles_get') {
|
|
426
435
|
return Promise.resolve({
|
|
@@ -430,7 +430,7 @@ describe('RBAC Functions', () => {
|
|
|
430
430
|
});
|
|
431
431
|
});
|
|
432
432
|
|
|
433
|
-
describe('
|
|
433
|
+
describe('data_rbac_roles_list', () => {
|
|
434
434
|
it('should list all user roles', async () => {
|
|
435
435
|
mockSupabase.rpc.mockResolvedValue({
|
|
436
436
|
data: [
|
|
@@ -454,7 +454,7 @@ describe('RBAC Functions', () => {
|
|
|
454
454
|
error: null
|
|
455
455
|
});
|
|
456
456
|
|
|
457
|
-
const result = await mockSupabase.rpc('
|
|
457
|
+
const result = await mockSupabase.rpc('data_rbac_roles_list', {
|
|
458
458
|
p_user_id: mockUser.id
|
|
459
459
|
});
|
|
460
460
|
|
|
@@ -476,7 +476,7 @@ describe('RBAC Functions', () => {
|
|
|
476
476
|
error: null
|
|
477
477
|
});
|
|
478
478
|
|
|
479
|
-
const result = await mockSupabase.rpc('
|
|
479
|
+
const result = await mockSupabase.rpc('data_rbac_roles_list', {
|
|
480
480
|
p_user_id: mockUser.id,
|
|
481
481
|
p_organisation_id: mockOrganisation.id
|
|
482
482
|
});
|
package/src/rbac/api.test.ts
CHANGED
|
@@ -191,6 +191,14 @@ describe('RBAC API', () => {
|
|
|
191
191
|
|
|
192
192
|
it('handles engine creation errors gracefully', () => {
|
|
193
193
|
const error = new Error('Engine creation error');
|
|
194
|
+
// Ensure createRBACConfig doesn't throw so we can test engine creation error
|
|
195
|
+
mockCreateRBACConfig.mockImplementation((config) => config);
|
|
196
|
+
mockGetRBACLogger.mockReturnValue({
|
|
197
|
+
info: vi.fn(),
|
|
198
|
+
warn: vi.fn(),
|
|
199
|
+
error: vi.fn(),
|
|
200
|
+
debug: vi.fn()
|
|
201
|
+
});
|
|
194
202
|
mockCreateRBACEngine.mockImplementation(() => {
|
|
195
203
|
throw error;
|
|
196
204
|
});
|
|
@@ -202,6 +210,16 @@ describe('RBAC API', () => {
|
|
|
202
210
|
|
|
203
211
|
it('handles audit manager creation errors gracefully', () => {
|
|
204
212
|
const error = new Error('Audit manager creation error');
|
|
213
|
+
// Ensure createRBACConfig doesn't throw so we can test audit manager creation error
|
|
214
|
+
mockCreateRBACConfig.mockImplementation((config) => config);
|
|
215
|
+
mockGetRBACLogger.mockReturnValue({
|
|
216
|
+
info: vi.fn(),
|
|
217
|
+
warn: vi.fn(),
|
|
218
|
+
error: vi.fn(),
|
|
219
|
+
debug: vi.fn()
|
|
220
|
+
});
|
|
221
|
+
const mockEngine = { id: 'test-engine' };
|
|
222
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
205
223
|
mockCreateAuditManager.mockImplementation(() => {
|
|
206
224
|
throw error;
|
|
207
225
|
});
|
|
@@ -309,6 +327,21 @@ describe('RBAC API', () => {
|
|
|
309
327
|
|
|
310
328
|
describe('Error Handling', () => {
|
|
311
329
|
it('handles missing supabase client', () => {
|
|
330
|
+
const mockEngine = { id: 'test-engine' };
|
|
331
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
332
|
+
const mockLogger = {
|
|
333
|
+
info: vi.fn(),
|
|
334
|
+
warn: vi.fn(),
|
|
335
|
+
error: vi.fn(),
|
|
336
|
+
debug: vi.fn()
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Ensure mocks don't throw
|
|
340
|
+
mockCreateRBACConfig.mockImplementation((config) => config);
|
|
341
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
342
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
343
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
344
|
+
|
|
312
345
|
// The function doesn't throw, it just creates config with null
|
|
313
346
|
expect(() => {
|
|
314
347
|
setupRBAC(null as any);
|
|
@@ -317,6 +350,20 @@ describe('RBAC API', () => {
|
|
|
317
350
|
|
|
318
351
|
it('handles invalid supabase client', () => {
|
|
319
352
|
const invalidSupabase = { invalid: 'client' };
|
|
353
|
+
const mockEngine = { id: 'test-engine' };
|
|
354
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
355
|
+
const mockLogger = {
|
|
356
|
+
info: vi.fn(),
|
|
357
|
+
warn: vi.fn(),
|
|
358
|
+
error: vi.fn(),
|
|
359
|
+
debug: vi.fn()
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Ensure mocks don't throw
|
|
363
|
+
mockCreateRBACConfig.mockImplementation((config) => config);
|
|
364
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
365
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
366
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
320
367
|
|
|
321
368
|
// The function doesn't throw, it just creates config with invalid client
|
|
322
369
|
expect(() => {
|
|
@@ -354,6 +401,21 @@ describe('RBAC API', () => {
|
|
|
354
401
|
|
|
355
402
|
describe('Configuration Validation', () => {
|
|
356
403
|
it('validates required supabase client', () => {
|
|
404
|
+
const mockEngine = { id: 'test-engine' };
|
|
405
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
406
|
+
const mockLogger = {
|
|
407
|
+
info: vi.fn(),
|
|
408
|
+
warn: vi.fn(),
|
|
409
|
+
error: vi.fn(),
|
|
410
|
+
debug: vi.fn()
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Ensure mocks don't throw
|
|
414
|
+
mockCreateRBACConfig.mockImplementation((config) => config);
|
|
415
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
416
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
417
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
418
|
+
|
|
357
419
|
// The function doesn't throw, it just creates config with undefined
|
|
358
420
|
expect(() => {
|
|
359
421
|
setupRBAC(undefined as any);
|
|
@@ -365,6 +427,20 @@ describe('RBAC API', () => {
|
|
|
365
427
|
from: 'not-a-function',
|
|
366
428
|
auth: 'not-an-object'
|
|
367
429
|
};
|
|
430
|
+
const mockEngine = { id: 'test-engine' };
|
|
431
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
432
|
+
const mockLogger = {
|
|
433
|
+
info: vi.fn(),
|
|
434
|
+
warn: vi.fn(),
|
|
435
|
+
error: vi.fn(),
|
|
436
|
+
debug: vi.fn()
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Ensure mocks don't throw
|
|
440
|
+
mockCreateRBACConfig.mockImplementation((config) => config);
|
|
441
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
442
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
443
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
368
444
|
|
|
369
445
|
// The function doesn't throw, it just creates config with invalid client
|
|
370
446
|
expect(() => {
|
|
@@ -415,6 +491,32 @@ describe('RBAC API', () => {
|
|
|
415
491
|
});
|
|
416
492
|
});
|
|
417
493
|
|
|
494
|
+
describe('Type Safety', () => {
|
|
495
|
+
it('accepts valid Supabase client types', () => {
|
|
496
|
+
const validSupabase = {
|
|
497
|
+
from: vi.fn(),
|
|
498
|
+
auth: { getUser: vi.fn() },
|
|
499
|
+
rpc: vi.fn()
|
|
500
|
+
};
|
|
501
|
+
const mockEngine = { id: 'test-engine' };
|
|
502
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
503
|
+
const mockLogger = {
|
|
504
|
+
info: vi.fn(),
|
|
505
|
+
warn: vi.fn(),
|
|
506
|
+
error: vi.fn(),
|
|
507
|
+
debug: vi.fn()
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
511
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
512
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
513
|
+
|
|
514
|
+
expect(() => {
|
|
515
|
+
setupRBAC(validSupabase as any);
|
|
516
|
+
}).not.toThrow();
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
418
520
|
describe('Cache Integration', () => {
|
|
419
521
|
it('initializes cache correctly', () => {
|
|
420
522
|
const mockEngine = { id: 'test-engine' };
|
|
@@ -456,6 +558,8 @@ describe('RBAC API', () => {
|
|
|
456
558
|
debug: vi.fn()
|
|
457
559
|
};
|
|
458
560
|
|
|
561
|
+
// Ensure mocks don't throw
|
|
562
|
+
mockCreateRBACConfig.mockImplementation((config) => config);
|
|
459
563
|
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
460
564
|
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
461
565
|
mockGetRBACLogger.mockReturnValue(mockLogger);
|
package/src/rbac/engine.ts
CHANGED
|
@@ -440,7 +440,7 @@ export class RBACEngine {
|
|
|
440
440
|
async resolveAppContext(input: { userId: UUID; appName: string }): Promise<RBACAppContext | null> {
|
|
441
441
|
try {
|
|
442
442
|
const { userId, appName } = input;
|
|
443
|
-
const { data, error } = await (this.supabase as any).rpc('
|
|
443
|
+
const { data, error } = await (this.supabase as any).rpc('data_app_resolve', {
|
|
444
444
|
p_user_id: userId,
|
|
445
445
|
p_app_name: appName,
|
|
446
446
|
});
|
|
@@ -647,7 +647,7 @@ describe('useCan Hook', () => {
|
|
|
647
647
|
}, { timeout: 20000 });
|
|
648
648
|
});
|
|
649
649
|
|
|
650
|
-
it('times out after 3 seconds when organisationId is missing', async () => {
|
|
650
|
+
it('times out after 3 seconds when organisationId is missing', { timeout: 10000 }, async () => {
|
|
651
651
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
652
652
|
|
|
653
653
|
const emptyScope = {} as any;
|
|
@@ -672,7 +672,7 @@ describe('useCan Hook', () => {
|
|
|
672
672
|
expect(result.current.error).not.toBeNull();
|
|
673
673
|
expect(result.current.error?.message).toBe('Organisation context is required for permission checks');
|
|
674
674
|
}, { timeout: 2000 });
|
|
675
|
-
}
|
|
675
|
+
});
|
|
676
676
|
|
|
677
677
|
it('handles null userId', async () => {
|
|
678
678
|
const { result } = renderHook(() =>
|
package/src/rbac/secureClient.ts
CHANGED
|
@@ -536,7 +536,7 @@ export class SecureSupabaseClient {
|
|
|
536
536
|
// 2. Add it here with the parameters it accepts
|
|
537
537
|
const rpcContextWhitelist: Record<string, Set<string>> = {
|
|
538
538
|
// RPCs that accept all three context parameters
|
|
539
|
-
'
|
|
539
|
+
'data_rbac_roles_list': new Set(['p_organisation_id', 'p_event_id', 'p_app_id']),
|
|
540
540
|
|
|
541
541
|
// RPCs that accept only p_organisation_id (not p_app_id or p_event_id)
|
|
542
542
|
'data_file_reference_by_category_list': new Set(['p_organisation_id']),
|
|
@@ -185,7 +185,7 @@ export enum RPCFunction {
|
|
|
185
185
|
// Role Management Functions
|
|
186
186
|
RBAC_ROLE_GRANT = 'rbac_role_grant',
|
|
187
187
|
RBAC_ROLE_REVOKE = 'rbac_role_revoke',
|
|
188
|
-
RBAC_ROLES_LIST = '
|
|
188
|
+
RBAC_ROLES_LIST = 'data_rbac_roles_list',
|
|
189
189
|
RBAC_ROLE_VALIDATE = 'rbac_role_validate',
|
|
190
190
|
|
|
191
191
|
// Session & Audit Functions
|
|
@@ -88,7 +88,7 @@ describe('parseAndNormalizeEventColours', () => {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
describe('Standard format only', () => {
|
|
91
|
-
it('
|
|
91
|
+
it('supports ev-main, ev-sec, ev-acc keys and normalizes them', () => {
|
|
92
92
|
const input = {
|
|
93
93
|
'ev-main': { 500: { L: 0.5, C: 0.2, H: 0 } },
|
|
94
94
|
'ev-sec': { 500: { L: 0.5, C: 0.2, H: 120 } },
|
|
@@ -96,14 +96,11 @@ describe('parseAndNormalizeEventColours', () => {
|
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
const result = parseAndNormalizeEventColours(input);
|
|
99
|
-
//
|
|
99
|
+
// ev-main, ev-sec, ev-acc keys are supported and normalized to main, sec, acc
|
|
100
100
|
expect(result).not.toBeNull();
|
|
101
|
-
expect(result).
|
|
102
|
-
expect(result).
|
|
103
|
-
expect(result).
|
|
104
|
-
expect(result?.main).toHaveProperty('500');
|
|
105
|
-
expect(result?.sec).toHaveProperty('500');
|
|
106
|
-
expect(result?.acc).toHaveProperty('500');
|
|
101
|
+
expect(result?.main[500]).toEqual({ L: 0.5, C: 0.2, H: 0 });
|
|
102
|
+
expect(result?.sec[500]).toEqual({ L: 0.5, C: 0.2, H: 120 });
|
|
103
|
+
expect(result?.acc[500]).toEqual({ L: 0.5, C: 0.2, H: 240 });
|
|
107
104
|
});
|
|
108
105
|
|
|
109
106
|
it('uses standard keys when provided', () => {
|
|
@@ -198,6 +195,118 @@ describe('parseAndNormalizeEventColours', () => {
|
|
|
198
195
|
});
|
|
199
196
|
});
|
|
200
197
|
|
|
198
|
+
describe('ev- prefix handling for shade names', () => {
|
|
199
|
+
it('strips ev- prefix from shade names', () => {
|
|
200
|
+
const input = {
|
|
201
|
+
main: {
|
|
202
|
+
'ev-main-500': { L: 0.5, C: 0.2, H: 0 },
|
|
203
|
+
'ev-main-100': { L: 0.9, C: 0.1, H: 0 }
|
|
204
|
+
},
|
|
205
|
+
acc: {
|
|
206
|
+
'ev-acc-500': { L: 0.5, C: 0.2, H: 240 }
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const result = parseAndNormalizeEventColours(input);
|
|
211
|
+
expect(result).not.toBeNull();
|
|
212
|
+
|
|
213
|
+
// ev- prefix should be stripped from shade names
|
|
214
|
+
expect(result?.main['main-500']).toEqual({ L: 0.5, C: 0.2, H: 0 });
|
|
215
|
+
expect(result?.main['main-100']).toEqual({ L: 0.9, C: 0.1, H: 0 });
|
|
216
|
+
expect(result?.acc['acc-500']).toEqual({ L: 0.5, C: 0.2, H: 240 });
|
|
217
|
+
|
|
218
|
+
// Original keys with ev- prefix should not exist
|
|
219
|
+
expect(result?.main['ev-main-500']).toBeUndefined();
|
|
220
|
+
expect(result?.main['ev-main-100']).toBeUndefined();
|
|
221
|
+
expect(result?.acc['ev-acc-500']).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('handles mixed ev- prefixed and non-prefixed shade names', () => {
|
|
225
|
+
const input = {
|
|
226
|
+
main: {
|
|
227
|
+
'ev-main-500': { L: 0.5, C: 0.2, H: 0 },
|
|
228
|
+
100: { L: 0.9, C: 0.1, H: 0 },
|
|
229
|
+
raw: { L: 0.55, C: 0.25, H: 5 }
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = parseAndNormalizeEventColours(input);
|
|
234
|
+
expect(result).not.toBeNull();
|
|
235
|
+
|
|
236
|
+
// ev- prefixed shade should be normalized
|
|
237
|
+
expect(result?.main['main-500']).toEqual({ L: 0.5, C: 0.2, H: 0 });
|
|
238
|
+
|
|
239
|
+
// Non-prefixed shades should remain unchanged
|
|
240
|
+
expect(result?.main[100]).toEqual({ L: 0.9, C: 0.1, H: 0 });
|
|
241
|
+
expect(result?.main.raw).toEqual({ L: 0.55, C: 0.25, H: 5 });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('handles ev- prefixed shade names in sec palette', () => {
|
|
245
|
+
const input = {
|
|
246
|
+
sec: {
|
|
247
|
+
'ev-sec-500': { L: 0.5, C: 0.2, H: 120 },
|
|
248
|
+
'ev-sec-200': { L: 0.8, C: 0.15, H: 120 }
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = parseAndNormalizeEventColours(input);
|
|
253
|
+
expect(result).not.toBeNull();
|
|
254
|
+
|
|
255
|
+
expect(result?.sec['sec-500']).toEqual({ L: 0.5, C: 0.2, H: 120 });
|
|
256
|
+
expect(result?.sec['sec-200']).toEqual({ L: 0.8, C: 0.15, H: 120 });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('preserves shade names that do not start with ev-', () => {
|
|
260
|
+
const input = {
|
|
261
|
+
main: {
|
|
262
|
+
500: { L: 0.5, C: 0.2, H: 0 },
|
|
263
|
+
'main-500': { L: 0.6, C: 0.3, H: 0 },
|
|
264
|
+
raw: { L: 0.55, C: 0.25, H: 5 }
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const result = parseAndNormalizeEventColours(input);
|
|
269
|
+
expect(result).not.toBeNull();
|
|
270
|
+
|
|
271
|
+
// All shades should be preserved as-is (no ev- prefix to strip)
|
|
272
|
+
expect(result?.main[500]).toEqual({ L: 0.5, C: 0.2, H: 0 });
|
|
273
|
+
expect(result?.main['main-500']).toEqual({ L: 0.6, C: 0.3, H: 0 });
|
|
274
|
+
expect(result?.main.raw).toEqual({ L: 0.55, C: 0.25, H: 5 });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('normalizes both palette keys and shade names with ev- prefix together', () => {
|
|
278
|
+
const input = {
|
|
279
|
+
'ev-main': {
|
|
280
|
+
'ev-main-raw': { L: 0.55, C: 0.25, H: 5 },
|
|
281
|
+
'ev-main-200': { L: 0.8, C: 0.15, H: 0 }
|
|
282
|
+
},
|
|
283
|
+
'ev-sec': {
|
|
284
|
+
'ev-sec-200': { L: 0.8, C: 0.15, H: 120 },
|
|
285
|
+
'ev-sec-500': { L: 0.5, C: 0.2, H: 120 }
|
|
286
|
+
},
|
|
287
|
+
'ev-acc': {
|
|
288
|
+
'ev-acc-800': { L: 0.3, C: 0.2, H: 240 }
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const result = parseAndNormalizeEventColours(input);
|
|
293
|
+
expect(result).not.toBeNull();
|
|
294
|
+
|
|
295
|
+
// Palette keys should be normalized: ev-main -> main, ev-sec -> sec, ev-acc -> acc
|
|
296
|
+
// Shade names should be normalized: ev-main-raw -> main-raw, ev-sec-200 -> sec-200, etc.
|
|
297
|
+
expect(result?.main['main-raw']).toEqual({ L: 0.55, C: 0.25, H: 5 });
|
|
298
|
+
expect(result?.main['main-200']).toEqual({ L: 0.8, C: 0.15, H: 0 });
|
|
299
|
+
expect(result?.sec['sec-200']).toEqual({ L: 0.8, C: 0.15, H: 120 });
|
|
300
|
+
expect(result?.sec['sec-500']).toEqual({ L: 0.5, C: 0.2, H: 120 });
|
|
301
|
+
expect(result?.acc['acc-800']).toEqual({ L: 0.3, C: 0.2, H: 240 });
|
|
302
|
+
|
|
303
|
+
// Original keys should not exist
|
|
304
|
+
expect(result?.main['ev-main-raw']).toBeUndefined();
|
|
305
|
+
expect(result?.sec['ev-sec-200']).toBeUndefined();
|
|
306
|
+
expect(result?.acc['ev-acc-800']).toBeUndefined();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
201
310
|
describe('Error handling', () => {
|
|
202
311
|
it('handles malformed color objects gracefully', () => {
|
|
203
312
|
const input = {
|
|
@@ -15,12 +15,21 @@
|
|
|
15
15
|
* Supports input formats:
|
|
16
16
|
* - Object with 'main', 'sec', 'acc' keys (standard format)
|
|
17
17
|
* - Object with 'ev-main', 'ev-sec', 'ev-acc' keys (database format with prefix)
|
|
18
|
+
* - Shade names with 'ev-' prefix (e.g., 'ev-acc-500' -> 'acc-500', 'ev-main-raw' -> 'main-raw')
|
|
18
19
|
* - JSON string that will be parsed
|
|
19
20
|
*
|
|
20
21
|
* Only includes explicitly defined color values. Does not fill
|
|
21
22
|
* missing shades - only shades that are present in the input will
|
|
22
23
|
* be included in the output.
|
|
23
24
|
*
|
|
25
|
+
* The parser automatically strips 'ev-' prefixes from BOTH palette keys and shade names
|
|
26
|
+
* for future-proofing, allowing distinction between event colors and organization colors
|
|
27
|
+
* (org colors not yet implemented).
|
|
28
|
+
*
|
|
29
|
+
* Normalization rules:
|
|
30
|
+
* - Palette keys: 'ev-main' -> 'main', 'ev-sec' -> 'sec', 'ev-acc' -> 'acc'
|
|
31
|
+
* - Shade names: 'ev-main-raw' -> 'main-raw', 'ev-sec-200' -> 'sec-200', 'ev-acc-800' -> 'acc-800'
|
|
32
|
+
*
|
|
24
33
|
* @param input - Event colours from database (JSONB field)
|
|
25
34
|
* @returns Normalized palette data with main, sec, acc palettes, or null if invalid
|
|
26
35
|
*
|
|
@@ -38,7 +47,7 @@
|
|
|
38
47
|
*
|
|
39
48
|
* @example
|
|
40
49
|
* ```ts
|
|
41
|
-
* // Database format with ev- prefix
|
|
50
|
+
* // Database format with ev- prefix on palette keys
|
|
42
51
|
* const colours = {
|
|
43
52
|
* 'ev-main': { 500: { L: 0.5, C: 0.2, H: 0 } },
|
|
44
53
|
* 'ev-sec': { 500: { L: 0.5, C: 0.2, H: 120 } },
|
|
@@ -48,6 +57,44 @@
|
|
|
48
57
|
* // Returns: { main: { 500: {...} }, sec: { 500: {...} }, acc: { 500: {...} } }
|
|
49
58
|
* ```
|
|
50
59
|
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* // Future-proofing: ev- prefix on shade names
|
|
63
|
+
* const colours = {
|
|
64
|
+
* main: {
|
|
65
|
+
* 'ev-main-500': { L: 0.5, C: 0.2, H: 0 },
|
|
66
|
+
* 'ev-main-raw': { L: 0.55, C: 0.25, H: 5 }
|
|
67
|
+
* },
|
|
68
|
+
* sec: {
|
|
69
|
+
* 'ev-sec-200': { L: 0.8, C: 0.15, H: 120 }
|
|
70
|
+
* },
|
|
71
|
+
* acc: {
|
|
72
|
+
* 'ev-acc-800': { L: 0.3, C: 0.2, H: 240 }
|
|
73
|
+
* }
|
|
74
|
+
* };
|
|
75
|
+
* const palette = parseAndNormalizeEventColours(colours);
|
|
76
|
+
* // Returns: { main: { 'main-500': {...}, 'main-raw': {...} }, sec: { 'sec-200': {...} }, acc: { 'acc-800': {...} } }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* // Both palette keys and shade names with ev- prefix
|
|
82
|
+
* const colours = {
|
|
83
|
+
* 'ev-main': {
|
|
84
|
+
* 'ev-main-raw': { L: 0.55, C: 0.25, H: 5 },
|
|
85
|
+
* 'ev-main-200': { L: 0.8, C: 0.15, H: 0 }
|
|
86
|
+
* },
|
|
87
|
+
* 'ev-sec': {
|
|
88
|
+
* 'ev-sec-500': { L: 0.5, C: 0.2, H: 120 }
|
|
89
|
+
* },
|
|
90
|
+
* 'ev-acc': {
|
|
91
|
+
* 'ev-acc-800': { L: 0.3, C: 0.2, H: 240 }
|
|
92
|
+
* }
|
|
93
|
+
* };
|
|
94
|
+
* const palette = parseAndNormalizeEventColours(colours);
|
|
95
|
+
* // Returns: { main: { 'main-raw': {...}, 'main-200': {...} }, sec: { 'sec-500': {...} }, acc: { 'acc-800': {...} } }
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
51
98
|
*/
|
|
52
99
|
export function parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
|
|
53
100
|
try {
|
|
@@ -78,6 +125,8 @@ export function parseAndNormalizeEventColours(input: unknown): { main: any; sec:
|
|
|
78
125
|
|
|
79
126
|
// Helper: only include explicitly defined color values
|
|
80
127
|
// This ensures we don't include undefined shades in the palette
|
|
128
|
+
// Also strips 'ev-' prefix from shade names for future-proofing (to distinguish event colors from org colors)
|
|
129
|
+
// Palette keys are normalized above (ev-main -> main), shade names are normalized here (ev-main-raw -> main-raw)
|
|
81
130
|
const fill = (p: any) => {
|
|
82
131
|
if (!p || typeof p !== 'object') return {} as any;
|
|
83
132
|
const out: any = {};
|
|
@@ -88,7 +137,12 @@ export function parseAndNormalizeEventColours(input: unknown): { main: any; sec:
|
|
|
88
137
|
// Only include the key if it has a defined value (not null, undefined, or empty)
|
|
89
138
|
const value = p[key];
|
|
90
139
|
if (value !== null && value !== undefined && value !== '') {
|
|
91
|
-
|
|
140
|
+
// Strip 'ev-' prefix from shade names for future-proofing
|
|
141
|
+
// e.g., 'ev-acc-500' -> 'acc-500', 'ev-main-raw' -> 'main-raw', 'ev-sec-200' -> 'sec-200'
|
|
142
|
+
const normalizedKey = typeof key === 'string' && key.startsWith('ev-')
|
|
143
|
+
? key.substring(3)
|
|
144
|
+
: key;
|
|
145
|
+
out[normalizedKey] = value;
|
|
92
146
|
}
|
|
93
147
|
}
|
|
94
148
|
|
package/src/types/supabase.ts
CHANGED
|
@@ -128,7 +128,7 @@ export type RPCFunction =
|
|
|
128
128
|
| 'rbac_page_access_check'
|
|
129
129
|
| 'rbac_role_grant'
|
|
130
130
|
| 'rbac_role_revoke'
|
|
131
|
-
| '
|
|
131
|
+
| 'data_rbac_roles_list'
|
|
132
132
|
| 'rbac_role_validate'
|
|
133
133
|
| 'rbac_session_track'
|
|
134
134
|
| 'rbac_audit_log'
|
|
@@ -137,8 +137,7 @@ export type RPCFunction =
|
|
|
137
137
|
| 'data_user_organisation_roles_get'
|
|
138
138
|
| 'data_user_organisations_get'
|
|
139
139
|
| 'data_cake_meals_get'
|
|
140
|
-
|
|
141
|
-
| 'util_app_resolve'
|
|
140
|
+
| 'data_app_resolve'
|
|
142
141
|
| 'debug_auth_context'
|
|
143
142
|
| 'handle_new_user'
|
|
144
143
|
// Other functions
|