@jmruthers/pace-core 0.5.190 → 0.5.191
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-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
- package/dist/{DataTable-ON3IXISJ.js → DataTable-WKRZD47S.js} +6 -6
- package/dist/{PublicPageProvider-C4uxosp6.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +1 -1
- package/dist/{UnifiedAuthProvider-X5NXANVI.js → UnifiedAuthProvider-FTSG5XH7.js} +3 -3
- package/dist/{api-I6UCQ5S6.js → api-IHKALJZD.js} +2 -2
- package/dist/{chunk-J2XXC7R5.js → chunk-6LTQQAT6.js} +77 -111
- package/dist/chunk-6LTQQAT6.js.map +1 -0
- package/dist/{chunk-STYK4OH2.js → chunk-6TQDD426.js} +10 -10
- package/dist/chunk-6TQDD426.js.map +1 -0
- package/dist/{chunk-DZWK57KZ.js → chunk-G37KK66H.js} +1 -1
- package/dist/{chunk-DZWK57KZ.js.map → chunk-G37KK66H.js.map} +1 -1
- package/dist/{chunk-73HSNNOQ.js → chunk-LOMZXPSN.js} +13 -13
- package/dist/{chunk-Y4BUBBHD.js → chunk-OETXORNB.js} +3 -3
- package/dist/{chunk-RUYZKXOD.js → chunk-ROXMHMY2.js} +5 -3
- package/dist/chunk-ROXMHMY2.js.map +1 -0
- package/dist/{chunk-SDMHPX3X.js → chunk-ULHIJK66.js} +56 -21
- package/dist/{chunk-SDMHPX3X.js.map → chunk-ULHIJK66.js.map} +1 -1
- package/dist/{chunk-VVBAW5A5.js → chunk-VKB2CO4Z.js} +46 -35
- package/dist/chunk-VKB2CO4Z.js.map +1 -0
- package/dist/{chunk-HQVPB5MZ.js → chunk-VRGWKHDB.js} +6 -6
- package/dist/{chunk-NIU6J6OX.js → chunk-XNYQOL3Z.js} +16 -16
- package/dist/chunk-XNYQOL3Z.js.map +1 -0
- package/dist/{chunk-4QYC5L4K.js → chunk-XYXSXPUK.js} +22 -27
- package/dist/chunk-XYXSXPUK.js.map +1 -0
- package/dist/components.d.ts +3 -3
- package/dist/components.js +8 -8
- package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
- package/dist/hooks.d.ts +12 -12
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +18 -23
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +6 -6
- package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-DxIDS4bC.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +1 -1
- package/dist/utils.d.ts +8 -8
- package/dist/utils.js +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.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/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.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/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.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/LoggerConfig.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/ParsedAddress.md +2 -2
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.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/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +2 -2
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.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/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.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 +2 -2
- 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/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.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 +16 -16
- package/docs/migration/README.md +18 -0
- package/docs/migration/database-changes-december-2025.md +767 -0
- package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
- package/package.json +1 -1
- package/src/__tests__/public-recipe-view.test.ts +10 -10
- package/src/__tests__/rls-policies.test.ts +13 -13
- package/src/components/AddressField/README.md +6 -6
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +35 -15
- package/src/components/Select/Select.test.tsx +4 -1
- package/src/components/Select/Select.tsx +60 -15
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +192 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +741 -0
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +703 -0
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +581 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +9 -8
- package/src/hooks/public/usePublicEvent.ts +8 -8
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/useFileDisplay.ts +8 -9
- package/src/hooks/useQueryCache.ts +6 -6
- package/src/hooks/useSecureDataAccess.test.ts +8 -8
- package/src/hooks/useSecureDataAccess.ts +15 -11
- package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
- package/src/rbac/hooks/useRBAC.simple.test.ts +95 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +2 -2
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +490 -0
- package/src/rbac/utils/eventContext.ts +5 -2
- package/src/services/AuthService.ts +37 -8
- package/src/services/OrganisationService.ts +92 -139
- package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
- package/src/services/__tests__/OrganisationService.test.ts +218 -86
- package/src/types/database.generated.ts +166 -201
- package/src/types/supabase.ts +2 -2
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
- package/src/utils/file-reference/index.ts +4 -4
- package/src/utils/google-places/googlePlacesUtils.ts +1 -1
- package/src/utils/google-places/types.ts +1 -1
- package/src/utils/request-deduplication.ts +4 -4
- package/src/utils/security/secureDataAccess.test.ts +1 -1
- package/src/utils/security/secureDataAccess.ts +7 -4
- package/src/utils/storage/README.md +1 -1
- package/dist/chunk-4QYC5L4K.js.map +0 -1
- package/dist/chunk-J2XXC7R5.js.map +0 -1
- package/dist/chunk-NIU6J6OX.js.map +0 -1
- package/dist/chunk-RUYZKXOD.js.map +0 -1
- package/dist/chunk-STYK4OH2.js.map +0 -1
- package/dist/chunk-VVBAW5A5.js.map +0 -1
- /package/dist/{DataTable-ON3IXISJ.js.map → DataTable-WKRZD47S.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-X5NXANVI.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
- /package/dist/{api-I6UCQ5S6.js.map → api-IHKALJZD.js.map} +0 -0
- /package/dist/{chunk-73HSNNOQ.js.map → chunk-LOMZXPSN.js.map} +0 -0
- /package/dist/{chunk-Y4BUBBHD.js.map → chunk-OETXORNB.js.map} +0 -0
- /package/dist/{chunk-HQVPB5MZ.js.map → chunk-VRGWKHDB.js.map} +0 -0
|
@@ -27,6 +27,15 @@ interface OrganisationRoleRpcResponse {
|
|
|
27
27
|
organisation_id: string;
|
|
28
28
|
role: 'org_admin' | 'leader' | 'member' | 'supporter';
|
|
29
29
|
status: 'active' | 'inactive' | 'suspended';
|
|
30
|
+
// Organisation fields from RPC
|
|
31
|
+
name?: string;
|
|
32
|
+
display_name?: string;
|
|
33
|
+
subscription_tier?: string;
|
|
34
|
+
settings?: unknown;
|
|
35
|
+
is_active?: boolean;
|
|
36
|
+
parent_id?: string;
|
|
37
|
+
organisation_created_at?: string;
|
|
38
|
+
organisation_updated_at?: string;
|
|
30
39
|
[key: string]: unknown;
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -358,159 +367,106 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
358
367
|
this.notify();
|
|
359
368
|
|
|
360
369
|
try {
|
|
361
|
-
// Get user's organisation
|
|
362
|
-
//
|
|
363
|
-
|
|
370
|
+
// Get user's organisation roles directly from rbac_organisation_roles table
|
|
371
|
+
// This queries the source table directly instead of using the RPC which filters to match core_organisation_memberships view
|
|
372
|
+
// We still filter to active, non-revoked roles for the org selector
|
|
373
|
+
// The join includes organisation data, so we don't need a separate query that might be filtered by RLS
|
|
374
|
+
let memberships, membershipError, organisations: Organisation[] = [];
|
|
364
375
|
try {
|
|
365
|
-
//
|
|
366
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
367
|
-
const timeoutId = setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 10000);
|
|
368
|
-
abortSignal.addEventListener('abort', () => {
|
|
369
|
-
clearTimeout(timeoutId);
|
|
370
|
-
reject(new Error('Request aborted'));
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
const rpcPromise = this.supabaseClient.rpc('data_user_organisation_roles_get', {
|
|
375
|
-
p_user_id: this.user.id,
|
|
376
|
-
p_organisation_id: null
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Check if request was aborted before making the call
|
|
376
|
+
// Check if request was aborted before making query
|
|
380
377
|
if (abortSignal.aborted) {
|
|
381
378
|
throw new Error('Request aborted');
|
|
382
379
|
}
|
|
380
|
+
|
|
381
|
+
const { data: rolesData, error: rolesError } = await this.supabaseClient
|
|
382
|
+
.from('rbac_organisation_roles')
|
|
383
|
+
.select(`
|
|
384
|
+
id,
|
|
385
|
+
user_id,
|
|
386
|
+
organisation_id,
|
|
387
|
+
role,
|
|
388
|
+
status,
|
|
389
|
+
granted_at,
|
|
390
|
+
granted_by,
|
|
391
|
+
revoked_at,
|
|
392
|
+
revoked_by,
|
|
393
|
+
notes,
|
|
394
|
+
created_at,
|
|
395
|
+
updated_at,
|
|
396
|
+
core_organisations!inner(
|
|
397
|
+
id,
|
|
398
|
+
name,
|
|
399
|
+
display_name,
|
|
400
|
+
subscription_tier,
|
|
401
|
+
settings,
|
|
402
|
+
is_active,
|
|
403
|
+
parent_id,
|
|
404
|
+
created_at,
|
|
405
|
+
updated_at
|
|
406
|
+
)
|
|
407
|
+
`)
|
|
408
|
+
.eq('user_id', this.user.id)
|
|
409
|
+
.eq('status', 'active')
|
|
410
|
+
.is('revoked_at', null);
|
|
383
411
|
|
|
384
|
-
|
|
412
|
+
if (rolesError) {
|
|
413
|
+
logger.error("OrganisationService", "Error loading organisation roles:", rolesError);
|
|
414
|
+
throw rolesError;
|
|
415
|
+
}
|
|
385
416
|
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
memberships = result.data?.filter((role) =>
|
|
390
|
-
['org_admin', 'leader', 'member', 'supporter'].includes(role.role)
|
|
391
|
-
).map((m) => ({
|
|
417
|
+
// Map to branded types and extract organisation data from the join
|
|
418
|
+
// The join already includes organisation data, so we don't need a separate query
|
|
419
|
+
memberships = rolesData?.map((m) => ({
|
|
392
420
|
...m,
|
|
393
421
|
user_id: assertUserId(m.user_id),
|
|
394
422
|
organisation_id: assertOrganisationId(m.organisation_id),
|
|
395
423
|
})) || [];
|
|
396
|
-
membershipError = result.error;
|
|
397
|
-
} catch (queryError) {
|
|
398
|
-
membershipError = queryError instanceof Error ? queryError : new Error(String(queryError));
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (membershipError) {
|
|
402
|
-
logger.error("OrganisationService", "Error loading memberships:", membershipError);
|
|
403
424
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
.
|
|
414
|
-
.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
revoked_by,
|
|
424
|
-
notes,
|
|
425
|
-
created_at,
|
|
426
|
-
updated_at,
|
|
427
|
-
organisations!inner(
|
|
428
|
-
id,
|
|
429
|
-
name,
|
|
430
|
-
display_name,
|
|
431
|
-
subscription_tier,
|
|
432
|
-
settings,
|
|
433
|
-
is_active,
|
|
434
|
-
parent_id,
|
|
435
|
-
created_at,
|
|
436
|
-
updated_at
|
|
437
|
-
)
|
|
438
|
-
`)
|
|
439
|
-
.eq('user_id', this.user.id)
|
|
440
|
-
.eq('status', 'active')
|
|
441
|
-
.is('revoked_at', null)
|
|
442
|
-
.in('role', ['org_admin', 'leader', 'member', 'supporter']);
|
|
443
|
-
|
|
444
|
-
if (fallbackError) {
|
|
445
|
-
logger.error("OrganisationService", "Fallback query also failed:", fallbackError);
|
|
446
|
-
throw membershipError; // Throw original error
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Map to branded types
|
|
450
|
-
memberships = fallbackData?.map((m) => ({
|
|
451
|
-
...m,
|
|
452
|
-
user_id: assertUserId(m.user_id),
|
|
453
|
-
organisation_id: assertOrganisationId(m.organisation_id),
|
|
454
|
-
})) || [];
|
|
455
|
-
membershipError = null;
|
|
456
|
-
} catch (fallbackErr) {
|
|
457
|
-
logger.error("OrganisationService", "Fallback query failed:", fallbackErr);
|
|
458
|
-
throw membershipError; // Throw original error
|
|
425
|
+
// Extract unique organisations from the join results
|
|
426
|
+
// Use a Map to deduplicate by organisation ID
|
|
427
|
+
// Supabase returns joined data nested under the relation name
|
|
428
|
+
const organisationsMap = new Map<string, Organisation>();
|
|
429
|
+
rolesData?.forEach((role: any) => {
|
|
430
|
+
// The join returns organisation data nested under 'core_organisations' key
|
|
431
|
+
const orgData = role.core_organisations;
|
|
432
|
+
if (orgData && role.organisation_id && !organisationsMap.has(role.organisation_id)) {
|
|
433
|
+
organisationsMap.set(role.organisation_id, {
|
|
434
|
+
id: orgData.id,
|
|
435
|
+
name: orgData.name,
|
|
436
|
+
display_name: orgData.display_name,
|
|
437
|
+
subscription_tier: orgData.subscription_tier,
|
|
438
|
+
settings: orgData.settings,
|
|
439
|
+
is_active: orgData.is_active,
|
|
440
|
+
parent_id: orgData.parent_id,
|
|
441
|
+
created_at: orgData.created_at,
|
|
442
|
+
updated_at: orgData.updated_at,
|
|
443
|
+
} as Organisation);
|
|
459
444
|
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
organisations = Array.from(organisationsMap.values());
|
|
448
|
+
|
|
449
|
+
// Extract organisations from join results
|
|
450
|
+
} catch (queryError) {
|
|
451
|
+
// Extract error message properly from Supabase error objects
|
|
452
|
+
if (queryError instanceof Error) {
|
|
453
|
+
membershipError = queryError;
|
|
454
|
+
} else if (queryError && typeof queryError === 'object' && 'message' in queryError) {
|
|
455
|
+
membershipError = new Error(String((queryError as any).message));
|
|
460
456
|
} else {
|
|
461
|
-
|
|
457
|
+
membershipError = new Error(String(queryError));
|
|
462
458
|
}
|
|
459
|
+
logger.error("OrganisationService", "Error loading organisation roles:", membershipError);
|
|
460
|
+
throw membershipError;
|
|
463
461
|
}
|
|
464
462
|
|
|
465
463
|
if (!memberships || memberships.length === 0) {
|
|
466
464
|
throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
|
|
467
465
|
}
|
|
468
466
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
.map((m) => m.organisation_id)
|
|
472
|
-
.filter((id: string) => {
|
|
473
|
-
// Better validation to prevent empty string UUID errors
|
|
474
|
-
if (!id || typeof id !== 'string') {
|
|
475
|
-
logger.warn("OrganisationService", "Invalid organisation ID (not string):", id);
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
|
-
const trimmedId = id.trim();
|
|
479
|
-
if (trimmedId === '') {
|
|
480
|
-
logger.warn("OrganisationService", "Empty organisation ID found");
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
// Validate UUID format
|
|
484
|
-
const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId);
|
|
485
|
-
if (!isValidUuid) {
|
|
486
|
-
logger.warn("OrganisationService", "Invalid UUID format:", trimmedId);
|
|
487
|
-
}
|
|
488
|
-
return isValidUuid;
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
if (organisationIds.length === 0) {
|
|
492
|
-
logger.warn("OrganisationService", "No valid organisation IDs found in memberships:", memberships);
|
|
493
|
-
throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Check if request was aborted before making organisations query
|
|
497
|
-
if (abortSignal.aborted) {
|
|
498
|
-
throw new Error('Request aborted');
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const { data: allOrganisations, error: orgError } = await this.supabaseClient
|
|
502
|
-
.from('organisations')
|
|
503
|
-
.select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at');
|
|
504
|
-
|
|
505
|
-
if (orgError) {
|
|
506
|
-
logger.error("OrganisationService", "Error loading organisations:", orgError);
|
|
507
|
-
throw orgError;
|
|
467
|
+
if (!organisations || organisations.length === 0) {
|
|
468
|
+
throw new Error('No organisations found in role data') as OrganisationSecurityError;
|
|
508
469
|
}
|
|
509
|
-
|
|
510
|
-
// Filter manually on the client side
|
|
511
|
-
const organisations = allOrganisations?.filter(org =>
|
|
512
|
-
organisationIds.includes(org.id)
|
|
513
|
-
) || [];
|
|
514
470
|
|
|
515
471
|
// Create a map of organisation_id to role from the memberships data
|
|
516
472
|
const roleMap = new Map<string, string>();
|
|
@@ -522,6 +478,8 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
522
478
|
const orgs = organisations as Organisation[];
|
|
523
479
|
const activeOrgs = orgs.filter(org => org.is_active);
|
|
524
480
|
|
|
481
|
+
// Filter to active organisations only
|
|
482
|
+
|
|
525
483
|
if (activeOrgs.length === 0) {
|
|
526
484
|
throw new Error('User has no access to active organisations') as OrganisationSecurityError;
|
|
527
485
|
}
|
|
@@ -549,16 +507,13 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
549
507
|
initialOrg = validPersistedOrg;
|
|
550
508
|
selectionMethod = 'persisted';
|
|
551
509
|
} else {
|
|
552
|
-
logger.warn("OrganisationService", "Persisted organisation not found in active orgs, clearing cache");
|
|
553
510
|
localStorage.removeItem('pace-core-selected-organisation');
|
|
554
511
|
}
|
|
555
512
|
} else {
|
|
556
|
-
logger.warn("OrganisationService", "Invalid persisted organisation ID, clearing cache");
|
|
557
513
|
localStorage.removeItem('pace-core-selected-organisation');
|
|
558
514
|
}
|
|
559
515
|
}
|
|
560
516
|
} catch (storageError) {
|
|
561
|
-
logger.warn("OrganisationService", "Failed to restore persisted organisation:", storageError);
|
|
562
517
|
// Clear potentially corrupted cache
|
|
563
518
|
localStorage.removeItem('pace-core-selected-organisation');
|
|
564
519
|
}
|
|
@@ -610,10 +565,8 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
610
565
|
} catch (err) {
|
|
611
566
|
const error = err as Error;
|
|
612
567
|
// "User has no access to active organisations" is a valid state for users without orgs (e.g., profile pages)
|
|
613
|
-
//
|
|
614
|
-
if (error.message
|
|
615
|
-
logger.warn("OrganisationService", "User has no active organisations (this is expected for users without organisation access):", error);
|
|
616
|
-
} else {
|
|
568
|
+
// Only log actual errors, not expected states
|
|
569
|
+
if (error.message !== 'User has no access to active organisations') {
|
|
617
570
|
logger.error("OrganisationService", "Failed to load organisations:", err);
|
|
618
571
|
}
|
|
619
572
|
this._error = error;
|
|
@@ -88,16 +88,29 @@ describe('OrganisationService Pagination & Validation', () => {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
mockSupabase.from.mockImplementation((table: string) => {
|
|
91
|
-
if (table === '
|
|
91
|
+
if (table === 'rbac_organisation_roles') {
|
|
92
92
|
return {
|
|
93
93
|
select: vi.fn().mockResolvedValue({
|
|
94
|
-
data: [
|
|
94
|
+
data: [
|
|
95
|
+
{
|
|
96
|
+
...mockMembership,
|
|
97
|
+
core_organisations: mockOrganisation
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
...mockMembership2,
|
|
101
|
+
core_organisations: mockOrganisation2
|
|
102
|
+
}
|
|
103
|
+
],
|
|
95
104
|
error: null
|
|
96
|
-
})
|
|
105
|
+
}),
|
|
106
|
+
eq: vi.fn().mockReturnThis(),
|
|
107
|
+
is: vi.fn().mockReturnThis()
|
|
97
108
|
};
|
|
98
109
|
}
|
|
99
110
|
return {
|
|
100
|
-
select: vi.fn().mockResolvedValue({ data: [], error: null })
|
|
111
|
+
select: vi.fn().mockResolvedValue({ data: [], error: null }),
|
|
112
|
+
eq: vi.fn().mockReturnThis(),
|
|
113
|
+
is: vi.fn().mockReturnThis()
|
|
101
114
|
};
|
|
102
115
|
});
|
|
103
116
|
|
|
@@ -175,16 +188,29 @@ describe('OrganisationService Pagination & Validation', () => {
|
|
|
175
188
|
});
|
|
176
189
|
|
|
177
190
|
mockSupabase.from.mockImplementation((table: string) => {
|
|
178
|
-
if (table === '
|
|
191
|
+
if (table === 'rbac_organisation_roles') {
|
|
179
192
|
return {
|
|
180
193
|
select: vi.fn().mockResolvedValue({
|
|
181
|
-
data: [
|
|
194
|
+
data: [
|
|
195
|
+
{
|
|
196
|
+
...mockMembership,
|
|
197
|
+
core_organisations: mockOrganisation
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
...inactiveMembership,
|
|
201
|
+
core_organisations: inactiveOrganisation
|
|
202
|
+
}
|
|
203
|
+
],
|
|
182
204
|
error: null
|
|
183
|
-
})
|
|
205
|
+
}),
|
|
206
|
+
eq: vi.fn().mockReturnThis(),
|
|
207
|
+
is: vi.fn().mockReturnThis()
|
|
184
208
|
};
|
|
185
209
|
}
|
|
186
210
|
return {
|
|
187
|
-
select: vi.fn().mockResolvedValue({ data: [], error: null })
|
|
211
|
+
select: vi.fn().mockResolvedValue({ data: [], error: null }),
|
|
212
|
+
eq: vi.fn().mockReturnThis(),
|
|
213
|
+
is: vi.fn().mockReturnThis()
|
|
188
214
|
};
|
|
189
215
|
});
|
|
190
216
|
|