@jmruthers/pace-core 0.6.2 → 0.6.3
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/CHANGELOG.md +45 -0
- package/cursor-rules/00-pace-core-compliance.mdc +34 -2
- package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
- package/dist/{DataTable-TPTKCX4D.js → DataTable-THFPBKTP.js} +9 -8
- package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
- package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
- package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
- package/dist/{api-MVVQZLJI.js → api-IAGWF3ZG.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/{chunk-SFZUDBL5.js → chunk-2T2IG7T7.js} +70 -56
- package/dist/chunk-2T2IG7T7.js.map +1 -0
- package/dist/{chunk-MMZ7JXPU.js → chunk-6Z7LTB3D.js} +13 -21
- package/dist/{chunk-MMZ7JXPU.js.map → chunk-6Z7LTB3D.js.map} +1 -1
- package/dist/{chunk-6J4GEEJR.js → chunk-CNCQDFLN.js} +53 -27
- package/dist/chunk-CNCQDFLN.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-EHMR7VYL.js → chunk-DWUBLJJM.js} +361 -187
- package/dist/chunk-DWUBLJJM.js.map +1 -0
- package/dist/{chunk-2UOI2FG5.js → chunk-HFZBI76P.js} +4 -4
- package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
- package/dist/{chunk-3XC4CPTD.js → chunk-PQBSKX33.js} +244 -5727
- package/dist/chunk-PQBSKX33.js.map +1 -0
- package/dist/chunk-QRPVRXYT.js +226 -0
- package/dist/chunk-QRPVRXYT.js.map +1 -0
- package/dist/{chunk-24UVZUZG.js → chunk-RWEBCB47.js} +129 -387
- package/dist/chunk-RWEBCB47.js.map +1 -0
- package/dist/{chunk-XWQCNGTQ.js → chunk-YDQHOZNA.js} +173 -79
- package/dist/chunk-YDQHOZNA.js.map +1 -0
- package/dist/{chunk-NECFR5MM.js → chunk-ZNIWI3UC.js} +562 -644
- package/dist/chunk-ZNIWI3UC.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +12 -13
- package/dist/contextValidator-3JNZKUTX.js +9 -0
- package/dist/contextValidator-3JNZKUTX.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +67 -27
- package/dist/rbac/index.js +15 -8
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
- package/dist/utils.js +5 -7
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +14 -16
- package/docs/api/modules.md +3796 -2513
- package/docs/components/context-selector.md +126 -0
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- package/docs/rbac/secure-client-protection.md +330 -0
- package/package.json +3 -3
- package/scripts/audit/core/checks/compliance.cjs +72 -0
- package/scripts/audit/core/checks/dependencies.cjs +559 -28
- package/scripts/audit/core/checks/documentation.cjs +68 -3
- package/scripts/audit/core/checks/environment.cjs +2 -14
- package/scripts/audit/core/checks/error-handling.cjs +47 -6
- package/src/components/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/components/RowComponent.tsx +19 -19
- package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
- package/src/components/Dialog/Dialog.tsx +29 -1
- package/src/components/FileDisplay/FileDisplay.tsx +42 -10
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +44 -45
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +135 -33
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +2 -2
- package/src/components/index.ts +5 -5
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/useAppConfig.ts +15 -30
- package/src/hooks/useFileDisplay.ts +77 -50
- package/src/index.ts +4 -5
- package/src/providers/services/AuthServiceProvider.tsx +17 -7
- package/src/providers/services/EventServiceProvider.tsx +33 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/adapters.tsx +2 -2
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +178 -132
- package/src/rbac/components/PagePermissionGuard.tsx +38 -10
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
- package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
- package/src/rbac/hooks/permissions/useCan.ts +41 -11
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +0 -9
- package/src/rbac/hooks/useRBAC.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.ts +36 -37
- package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
- package/src/rbac/hooks/useResolvedScope.ts +35 -40
- package/src/rbac/hooks/useSecureSupabase.ts +7 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +103 -16
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +1 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
- package/src/rbac/utils/clientSecurity.ts +93 -0
- package/src/rbac/utils/contextValidator.ts +77 -168
- package/src/services/AuthService.ts +39 -7
- package/src/services/EventService.ts +186 -54
- package/src/services/OrganisationService.ts +81 -14
- package/src/services/__tests__/EventService.test.ts +1 -2
- package/src/services/base/BaseService.ts +3 -0
- package/src/utils/dynamic/dynamicUtils.ts +7 -4
- package/dist/chunk-24UVZUZG.js.map +0 -1
- package/dist/chunk-3XC4CPTD.js.map +0 -1
- package/dist/chunk-6J4GEEJR.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-EHMR7VYL.js.map +0 -1
- package/dist/chunk-NECFR5MM.js.map +0 -1
- package/dist/chunk-SFZUDBL5.js.map +0 -1
- package/dist/chunk-XWQCNGTQ.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/InvalidScopeError.md +0 -73
- package/docs/api/classes/Logger.md +0 -178
- package/docs/api/classes/MissingUserContextError.md +0 -66
- package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
- package/docs/api/classes/PermissionDeniedError.md +0 -73
- package/docs/api/classes/RBACAuditManager.md +0 -297
- package/docs/api/classes/RBACCache.md +0 -322
- package/docs/api/classes/RBACEngine.md +0 -171
- package/docs/api/classes/RBACError.md +0 -76
- package/docs/api/classes/RBACNotInitializedError.md +0 -66
- package/docs/api/classes/SecureSupabaseClient.md +0 -163
- package/docs/api/classes/StorageUtils.md +0 -328
- package/docs/api/enums/FileCategory.md +0 -184
- package/docs/api/enums/LogLevel.md +0 -54
- package/docs/api/enums/RBACErrorCode.md +0 -228
- package/docs/api/enums/RPCFunction.md +0 -118
- package/docs/api/interfaces/AddressFieldProps.md +0 -241
- package/docs/api/interfaces/AddressFieldRef.md +0 -94
- package/docs/api/interfaces/AggregateConfig.md +0 -43
- package/docs/api/interfaces/AutocompleteOptions.md +0 -75
- package/docs/api/interfaces/AvatarProps.md +0 -128
- package/docs/api/interfaces/BadgeProps.md +0 -34
- package/docs/api/interfaces/ButtonProps.md +0 -56
- package/docs/api/interfaces/CalendarProps.md +0 -73
- package/docs/api/interfaces/CardProps.md +0 -69
- package/docs/api/interfaces/ColorPalette.md +0 -7
- package/docs/api/interfaces/ColorShade.md +0 -66
- package/docs/api/interfaces/ComplianceResult.md +0 -30
- package/docs/api/interfaces/DataAccessRecord.md +0 -96
- package/docs/api/interfaces/DataRecord.md +0 -11
- package/docs/api/interfaces/DataTableAction.md +0 -252
- package/docs/api/interfaces/DataTableColumn.md +0 -504
- package/docs/api/interfaces/DataTableProps.md +0 -625
- package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
- package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
- package/docs/api/interfaces/DatabaseIssue.md +0 -41
- package/docs/api/interfaces/EmptyStateConfig.md +0 -61
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
- package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
- package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
- package/docs/api/interfaces/EventAppRoleData.md +0 -71
- package/docs/api/interfaces/ExportColumn.md +0 -90
- package/docs/api/interfaces/ExportOptions.md +0 -126
- package/docs/api/interfaces/FileDisplayProps.md +0 -249
- package/docs/api/interfaces/FileMetadata.md +0 -129
- package/docs/api/interfaces/FileReference.md +0 -118
- package/docs/api/interfaces/FileSizeLimits.md +0 -7
- package/docs/api/interfaces/FileUploadOptions.md +0 -139
- package/docs/api/interfaces/FileUploadProps.md +0 -296
- package/docs/api/interfaces/FooterProps.md +0 -107
- package/docs/api/interfaces/FormFieldProps.md +0 -166
- package/docs/api/interfaces/FormProps.md +0 -113
- package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
- package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
- package/docs/api/interfaces/InputProps.md +0 -56
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -187
- package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
- package/docs/api/interfaces/NavigationContextType.md +0 -164
- package/docs/api/interfaces/NavigationGuardProps.md +0 -139
- package/docs/api/interfaces/NavigationItem.md +0 -120
- package/docs/api/interfaces/NavigationMenuProps.md +0 -221
- package/docs/api/interfaces/NavigationProviderProps.md +0 -117
- package/docs/api/interfaces/Organisation.md +0 -140
- package/docs/api/interfaces/OrganisationContextType.md +0 -388
- package/docs/api/interfaces/OrganisationMembership.md +0 -140
- package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
- package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
- package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
- package/docs/api/interfaces/PageAccessRecord.md +0 -85
- package/docs/api/interfaces/PagePermissionContextType.md +0 -140
- package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
- package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
- package/docs/api/interfaces/PaletteData.md +0 -41
- package/docs/api/interfaces/ParsedAddress.md +0 -120
- package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
- package/docs/api/interfaces/ProgressProps.md +0 -42
- package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
- package/docs/api/interfaces/QuickFix.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
- package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
- package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
- package/docs/api/interfaces/RBACConfig.md +0 -133
- package/docs/api/interfaces/RBACContext.md +0 -52
- package/docs/api/interfaces/RBACLogger.md +0 -112
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
- package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
- package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
- package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
- package/docs/api/interfaces/RBACResult.md +0 -58
- package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
- package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
- package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
- package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
- package/docs/api/interfaces/RBACRolesListParams.md +0 -52
- package/docs/api/interfaces/RBACRolesListResult.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
- package/docs/api/interfaces/ResourcePermissions.md +0 -155
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
- package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
- package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
- package/docs/api/interfaces/RoleManagementResult.md +0 -52
- package/docs/api/interfaces/RouteAccessRecord.md +0 -107
- package/docs/api/interfaces/RouteConfig.md +0 -134
- package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
- package/docs/api/interfaces/SecureDataContextType.md +0 -168
- package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
- package/docs/api/interfaces/SetupIssue.md +0 -41
- package/docs/api/interfaces/StorageConfig.md +0 -41
- package/docs/api/interfaces/StorageFileInfo.md +0 -74
- package/docs/api/interfaces/StorageFileMetadata.md +0 -151
- package/docs/api/interfaces/StorageListOptions.md +0 -99
- package/docs/api/interfaces/StorageListResult.md +0 -41
- package/docs/api/interfaces/StorageUploadOptions.md +0 -101
- package/docs/api/interfaces/StorageUploadResult.md +0 -63
- package/docs/api/interfaces/StorageUrlOptions.md +0 -60
- package/docs/api/interfaces/StyleImport.md +0 -19
- package/docs/api/interfaces/SwitchProps.md +0 -34
- package/docs/api/interfaces/TabsContentProps.md +0 -9
- package/docs/api/interfaces/TabsListProps.md +0 -9
- package/docs/api/interfaces/TabsProps.md +0 -9
- package/docs/api/interfaces/TabsTriggerProps.md +0 -50
- package/docs/api/interfaces/TextareaProps.md +0 -53
- package/docs/api/interfaces/ToastActionElement.md +0 -12
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
- package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
- package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
- package/docs/api/interfaces/UserEventAccess.md +0 -121
- package/docs/api/interfaces/UserMenuProps.md +0 -88
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -423
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
- package/src/components/OrganisationSelector/index.ts +0 -9
- /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-THFPBKTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
- /package/dist/{api-MVVQZLJI.js.map → api-IAGWF3ZG.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-2UOI2FG5.js.map → chunk-HFZBI76P.js.map} +0 -0
- /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
|
@@ -23,6 +23,8 @@ type AuthStateSubscription = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export class AuthService extends BaseService implements IAuthService {
|
|
26
|
+
private static instanceCount = 0;
|
|
27
|
+
private instanceId: number;
|
|
26
28
|
private user: User | null = null;
|
|
27
29
|
private session: Session | null = null;
|
|
28
30
|
private authLoading = false;
|
|
@@ -43,12 +45,23 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
43
45
|
|
|
44
46
|
constructor(supabaseClient: SupabaseClient, appName?: string) {
|
|
45
47
|
super();
|
|
48
|
+
this.instanceId = ++AuthService.instanceCount;
|
|
46
49
|
this.supabaseClient = supabaseClient;
|
|
47
50
|
this.appName = appName;
|
|
51
|
+
logger.debug('AuthService', `Instance created [ID:${this.instanceId}]`, { appName });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getInstanceId(): number {
|
|
55
|
+
return this.instanceId;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
// Auth state getters
|
|
51
59
|
getUser(): User | null {
|
|
60
|
+
if (this.user) {
|
|
61
|
+
logger.debug('AuthService', `getUser() [ID:${this.instanceId}] returning user: ${this.user.id}`);
|
|
62
|
+
} else {
|
|
63
|
+
logger.debug('AuthService', `getUser() [ID:${this.instanceId}] returning null`);
|
|
64
|
+
}
|
|
52
65
|
return this.user;
|
|
53
66
|
}
|
|
54
67
|
|
|
@@ -398,6 +411,11 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
398
411
|
const subscription = this.supabaseClient.auth.onAuthStateChange(
|
|
399
412
|
(event: AuthChangeEvent, session: SupabaseSession | null) => {
|
|
400
413
|
try {
|
|
414
|
+
logger.debug('AuthService', `Auth state change [ID:${this.instanceId}]`, {
|
|
415
|
+
event,
|
|
416
|
+
hasSession: !!session,
|
|
417
|
+
userId: session?.user?.id
|
|
418
|
+
});
|
|
401
419
|
// Handle different auth events
|
|
402
420
|
if (event === 'SIGNED_OUT') {
|
|
403
421
|
this.session = null;
|
|
@@ -407,15 +425,15 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
407
425
|
// Automatic session tracking (non-blocking)
|
|
408
426
|
if (session?.user) {
|
|
409
427
|
this.trackSession('logout', session).catch(err => {
|
|
410
|
-
logger.warn('AuthService',
|
|
428
|
+
logger.warn('AuthService', `Failed to track logout session [ID:${this.instanceId}]:`, err);
|
|
411
429
|
});
|
|
412
430
|
}
|
|
413
431
|
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
|
|
414
432
|
this.session = session;
|
|
415
433
|
this.user = session?.user ?? null;
|
|
416
434
|
|
|
417
|
-
//
|
|
418
|
-
if (session) {
|
|
435
|
+
// Ensure state is set before non-blocking tracking calls
|
|
436
|
+
if (session?.user) {
|
|
419
437
|
this.authError = null;
|
|
420
438
|
}
|
|
421
439
|
|
|
@@ -423,7 +441,7 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
423
441
|
// Only track on SIGNED_IN, not TOKEN_REFRESHED (to avoid duplicate login records)
|
|
424
442
|
if (event === 'SIGNED_IN' && session?.user) {
|
|
425
443
|
this.trackSession('login', session).catch(err => {
|
|
426
|
-
logger.warn('AuthService',
|
|
444
|
+
logger.warn('AuthService', `Failed to track login session [ID:${this.instanceId}]:`, err);
|
|
427
445
|
});
|
|
428
446
|
}
|
|
429
447
|
} else if (event === 'INITIAL_SESSION') {
|
|
@@ -452,16 +470,30 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
452
470
|
// This ensures ProtectedRoute waits for session restoration to complete
|
|
453
471
|
// before checking authentication state
|
|
454
472
|
this.authLoading = false;
|
|
473
|
+
|
|
474
|
+
// Final check: Ensure state matches what we just logged
|
|
475
|
+
logger.debug('AuthService', `State synchronized after INITIAL_SESSION [ID:${this.instanceId}]`, {
|
|
476
|
+
hasUser: !!this.user,
|
|
477
|
+
userId: this.user?.id
|
|
478
|
+
});
|
|
479
|
+
|
|
455
480
|
this.notify();
|
|
456
481
|
return; // Return early to avoid setting loading to false again below
|
|
457
482
|
}
|
|
458
483
|
|
|
459
|
-
// For other events (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED), set loading to false
|
|
460
|
-
// INITIAL_SESSION is handled above and returns early
|
|
484
|
+
// For other events (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED), set loading to false and notify
|
|
461
485
|
this.authLoading = false;
|
|
486
|
+
|
|
487
|
+
// Final check: Ensure state matches what we just logged
|
|
488
|
+
logger.debug('AuthService', `State synchronized after event [ID:${this.instanceId}]`, {
|
|
489
|
+
event,
|
|
490
|
+
hasUser: !!this.user,
|
|
491
|
+
userId: this.user?.id
|
|
492
|
+
});
|
|
493
|
+
|
|
462
494
|
this.notify();
|
|
463
495
|
} catch (error) {
|
|
464
|
-
logger.warn('AuthService',
|
|
496
|
+
logger.warn('AuthService', `Error in auth state change handler [ID:${this.instanceId}]:`, error);
|
|
465
497
|
this.authLoading = false;
|
|
466
498
|
this.notify();
|
|
467
499
|
}
|
|
@@ -16,11 +16,12 @@ import { Organisation } from '../types/organisation';
|
|
|
16
16
|
import { assertOrganisationId } from '../types/core';
|
|
17
17
|
import { logger } from '../utils/core/logger';
|
|
18
18
|
import { secureStorage } from '../utils/security/secureStorage';
|
|
19
|
-
import { isSuperAdmin
|
|
19
|
+
import { isSuperAdmin } from '../rbac/api';
|
|
20
20
|
import type { UUID } from '../rbac/types';
|
|
21
|
-
import type { AppConfig } from '../rbac/utils/contextValidator';
|
|
22
21
|
|
|
23
22
|
export class EventService extends BaseService implements IEventService {
|
|
23
|
+
private static instanceCount = 0;
|
|
24
|
+
private instanceId: number;
|
|
24
25
|
private events: Event[] = [];
|
|
25
26
|
private selectedEvent: Event | null = null;
|
|
26
27
|
private _isLoading = false; // Start as false to avoid blocking UI
|
|
@@ -34,7 +35,7 @@ export class EventService extends BaseService implements IEventService {
|
|
|
34
35
|
private selectedOrganisation: Organisation | null = null;
|
|
35
36
|
private setSelectedEventId: ((eventId: string | null) => void) | null = null;
|
|
36
37
|
private isSuperAdmin: boolean = false; // Track super admin status for conditional validation
|
|
37
|
-
|
|
38
|
+
// App config removed - scope is now page-level only (rbac_app_pages.scope_type)
|
|
38
39
|
|
|
39
40
|
// Internal state management
|
|
40
41
|
private isInitializedRef = false;
|
|
@@ -51,12 +52,22 @@ export class EventService extends BaseService implements IEventService {
|
|
|
51
52
|
setSelectedEventId: (eventId: string | null) => void
|
|
52
53
|
) {
|
|
53
54
|
super();
|
|
55
|
+
this.instanceId = ++EventService.instanceCount;
|
|
54
56
|
this.supabaseClient = supabaseClient;
|
|
55
57
|
this.user = user;
|
|
56
58
|
this.session = session;
|
|
57
59
|
this.appName = appName;
|
|
58
60
|
this.selectedOrganisation = selectedOrganisation;
|
|
59
61
|
this.setSelectedEventId = setSelectedEventId;
|
|
62
|
+
logger.debug('EventService', `Instance created [ID:${this.instanceId}]`, {
|
|
63
|
+
appName,
|
|
64
|
+
hasUser: !!user,
|
|
65
|
+
userId: user?.id
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getInstanceId(): number {
|
|
70
|
+
return this.instanceId;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
// Helper method to get user-scoped storage key
|
|
@@ -97,6 +108,12 @@ export class EventService extends BaseService implements IEventService {
|
|
|
97
108
|
this.resetInitialization();
|
|
98
109
|
this.isInitializedRef = false;
|
|
99
110
|
this.isFetchingRef = false;
|
|
111
|
+
|
|
112
|
+
logger.debug('EventService', `User changed [ID:${this.instanceId}]`, {
|
|
113
|
+
previousUserId,
|
|
114
|
+
newUserId,
|
|
115
|
+
willInitialize: newUserId !== null
|
|
116
|
+
});
|
|
100
117
|
}
|
|
101
118
|
|
|
102
119
|
this.supabaseClient = supabaseClient;
|
|
@@ -106,19 +123,43 @@ export class EventService extends BaseService implements IEventService {
|
|
|
106
123
|
this.selectedOrganisation = selectedOrganisation;
|
|
107
124
|
this.setSelectedEventId = setSelectedEventId;
|
|
108
125
|
|
|
109
|
-
//
|
|
110
|
-
if (previousAppName !== appName) {
|
|
111
|
-
this.appConfig = null;
|
|
112
|
-
}
|
|
126
|
+
// App name changed - state will be reset by updateDependencies
|
|
113
127
|
|
|
114
128
|
// Update super admin status when user changes
|
|
115
129
|
// This allows super admins to select events from any organisation
|
|
130
|
+
// RBAC should be initialized synchronously by UnifiedAuthProvider, so this should always work
|
|
116
131
|
if (user?.id) {
|
|
117
132
|
try {
|
|
118
|
-
|
|
133
|
+
const { isRBACInitialized, isSuperAdmin: checkSuperAdmin, setupRBAC } = await import('../rbac/api');
|
|
134
|
+
|
|
135
|
+
// Ensure RBAC is initialized if possible (defensive check for edge cases)
|
|
136
|
+
if (!isRBACInitialized() && this.supabaseClient) {
|
|
137
|
+
setupRBAC(this.supabaseClient);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isRBACInitialized()) {
|
|
141
|
+
this.isSuperAdmin = await checkSuperAdmin(user.id as UUID);
|
|
142
|
+
logger.debug('EventService', 'Super admin status updated in updateDependencies', {
|
|
143
|
+
userId: user.id,
|
|
144
|
+
isSuperAdmin: this.isSuperAdmin
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
// RBAC not initialized - this should be rare since UnifiedAuthProvider initializes it synchronously
|
|
148
|
+
// Keep existing value (don't reset to false) to avoid clearing a previously determined super admin status
|
|
149
|
+
logger.warn('EventService', 'RBAC not initialized in updateDependencies, keeping existing super admin status', {
|
|
150
|
+
userId: user.id,
|
|
151
|
+
existingIsSuperAdmin: this.isSuperAdmin,
|
|
152
|
+
note: 'RBAC should be initialized by UnifiedAuthProvider. This may indicate EventService is being used outside the provider.'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
119
155
|
} catch (error) {
|
|
120
|
-
logger.warn('EventService', 'Failed to check super admin status', {
|
|
121
|
-
|
|
156
|
+
logger.warn('EventService', 'Failed to check super admin status in updateDependencies', {
|
|
157
|
+
error,
|
|
158
|
+
userId: user.id,
|
|
159
|
+
existingIsSuperAdmin: this.isSuperAdmin
|
|
160
|
+
});
|
|
161
|
+
// Don't reset to false on error - keep existing value to avoid blocking super admins
|
|
162
|
+
// The error might be transient, and we don't want to clear a valid super admin status
|
|
122
163
|
}
|
|
123
164
|
} else {
|
|
124
165
|
this.isSuperAdmin = false;
|
|
@@ -131,14 +172,26 @@ export class EventService extends BaseService implements IEventService {
|
|
|
131
172
|
this.resetInitialization(); // Reset BaseService's isInitialized flag
|
|
132
173
|
this.isInitializedRef = false;
|
|
133
174
|
this.isFetchingRef = false;
|
|
175
|
+
|
|
176
|
+
// SECURITY: Super admins can see events regardless of organisation context
|
|
177
|
+
// Do not clear events for super admins when organisation context is removed
|
|
178
|
+
const shouldClearEvents = !this.isSuperAdmin;
|
|
179
|
+
|
|
134
180
|
// Clear events ONLY when switching between different organisations (not when org first becomes available)
|
|
135
181
|
if (previousOrgId !== null && newOrgId !== null && previousOrgId !== newOrgId) {
|
|
136
|
-
|
|
137
|
-
|
|
182
|
+
if (shouldClearEvents) {
|
|
183
|
+
this.events = [];
|
|
184
|
+
// Use setSelectedEvent(null) to preserve userClearedEventRef flag if user explicitly cleared
|
|
185
|
+
// This prevents auto-selection from re-selecting the event after org switch
|
|
186
|
+
this.setSelectedEvent(null);
|
|
187
|
+
}
|
|
138
188
|
} else if (previousOrgId !== null && newOrgId === null) {
|
|
139
|
-
// Organisation was removed - clear events
|
|
140
|
-
|
|
141
|
-
|
|
189
|
+
// Organisation was removed - clear events if not super admin
|
|
190
|
+
if (shouldClearEvents) {
|
|
191
|
+
this.events = [];
|
|
192
|
+
// Use setSelectedEvent(null) to preserve userClearedEventRef flag if user explicitly cleared
|
|
193
|
+
this.setSelectedEvent(null);
|
|
194
|
+
}
|
|
142
195
|
}
|
|
143
196
|
}
|
|
144
197
|
|
|
@@ -220,7 +273,7 @@ export class EventService extends BaseService implements IEventService {
|
|
|
220
273
|
const persistedEvent = events.find(event => event.event_id === persistedEventId);
|
|
221
274
|
|
|
222
275
|
if (persistedEvent) {
|
|
223
|
-
// Use setSelectedEvent() to
|
|
276
|
+
// Use setSelectedEvent() to ensure consistent behavior
|
|
224
277
|
// This ensures consistent behavior and proper notification
|
|
225
278
|
// Theme will be applied by useEventTheme hook once user navigates away from login
|
|
226
279
|
this.setSelectedEvent(persistedEvent);
|
|
@@ -315,11 +368,13 @@ export class EventService extends BaseService implements IEventService {
|
|
|
315
368
|
protected async doInitialize(): Promise<void> {
|
|
316
369
|
// Skip if already initialized
|
|
317
370
|
if (this.isInitializedRef) {
|
|
371
|
+
logger.debug('EventService', 'Skipping initialization - already initialized');
|
|
318
372
|
return;
|
|
319
373
|
}
|
|
320
374
|
|
|
321
375
|
// Skip if already fetching
|
|
322
376
|
if (this.isFetchingRef) {
|
|
377
|
+
logger.debug('EventService', 'Skipping initialization - already fetching');
|
|
323
378
|
return;
|
|
324
379
|
}
|
|
325
380
|
|
|
@@ -336,14 +391,27 @@ export class EventService extends BaseService implements IEventService {
|
|
|
336
391
|
// For event-required apps, selectedOrganisation may be null (org derived from event)
|
|
337
392
|
// For org-required apps, selectedOrganisation is required
|
|
338
393
|
if (!this.user) {
|
|
394
|
+
logger.debug('EventService', 'Skipping initialization - no user');
|
|
339
395
|
return;
|
|
340
396
|
}
|
|
341
397
|
|
|
398
|
+
logger.debug('EventService', 'Initializing', {
|
|
399
|
+
userId: this.user.id,
|
|
400
|
+
appName: this.appName,
|
|
401
|
+
hasSelectedOrganisation: !!this.selectedOrganisation,
|
|
402
|
+
hasSupabaseClient: !!this.supabaseClient,
|
|
403
|
+
hasSession: !!this.session
|
|
404
|
+
});
|
|
405
|
+
|
|
342
406
|
// Initial setup - fetch events on initialization
|
|
343
407
|
await this.fetchEvents(false);
|
|
344
408
|
|
|
345
409
|
// Mark as initialized after successful fetch
|
|
346
410
|
this.isInitializedRef = true;
|
|
411
|
+
logger.debug('EventService', 'Initialization complete', {
|
|
412
|
+
eventsCount: this.events.length,
|
|
413
|
+
hasError: !!this.error
|
|
414
|
+
});
|
|
347
415
|
}
|
|
348
416
|
|
|
349
417
|
protected doCleanup(): void {
|
|
@@ -372,18 +440,7 @@ export class EventService extends BaseService implements IEventService {
|
|
|
372
440
|
let isMounted = true;
|
|
373
441
|
|
|
374
442
|
try {
|
|
375
|
-
//
|
|
376
|
-
if (!this.appConfig && this.appName) {
|
|
377
|
-
try {
|
|
378
|
-
this.appConfig = await getAppConfigByName(this.appName);
|
|
379
|
-
} catch (configError) {
|
|
380
|
-
logger.warn('EventService', 'Failed to load app config, defaulting to event-required', {
|
|
381
|
-
error: configError
|
|
382
|
-
});
|
|
383
|
-
// Default to event-required for safety
|
|
384
|
-
this.appConfig = { requires_event: true };
|
|
385
|
-
}
|
|
386
|
-
}
|
|
443
|
+
// Scope is now page-level only - no app-level config needed
|
|
387
444
|
|
|
388
445
|
// Determine organisationId for RPC call
|
|
389
446
|
// For event-required apps: org is derived from selectedEvent (if available), or null to get all accessible events
|
|
@@ -392,58 +449,119 @@ export class EventService extends BaseService implements IEventService {
|
|
|
392
449
|
let organisationIdForRpc: string | null = null;
|
|
393
450
|
|
|
394
451
|
// Check if user is super admin first
|
|
395
|
-
|
|
452
|
+
// RBAC should be initialized synchronously by UnifiedAuthProvider before EventService is used
|
|
453
|
+
// Use cached value as fallback for edge cases (e.g., EventService used outside UnifiedAuthProvider)
|
|
454
|
+
let userIsSuperAdmin = this.isSuperAdmin; // Start with cached value from updateDependencies
|
|
396
455
|
try {
|
|
397
|
-
|
|
456
|
+
const { isRBACInitialized, isSuperAdmin: checkSuperAdmin, setupRBAC } = await import('../rbac/api');
|
|
457
|
+
|
|
458
|
+
// Ensure RBAC is initialized if possible (defensive check for edge cases)
|
|
459
|
+
if (!isRBACInitialized() && this.supabaseClient) {
|
|
460
|
+
setupRBAC(this.supabaseClient);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Check super admin status if RBAC is ready
|
|
464
|
+
// RBAC should always be initialized by UnifiedAuthProvider, but we check defensively
|
|
465
|
+
if (isRBACInitialized()) {
|
|
466
|
+
userIsSuperAdmin = await checkSuperAdmin(this.user.id as UUID);
|
|
467
|
+
// Update cached value for future use
|
|
468
|
+
this.isSuperAdmin = userIsSuperAdmin;
|
|
469
|
+
logger.debug('EventService', 'Super admin check completed', {
|
|
470
|
+
userId: this.user.id,
|
|
471
|
+
isSuperAdmin: userIsSuperAdmin
|
|
472
|
+
});
|
|
473
|
+
} else {
|
|
474
|
+
// RBAC not initialized - this should be rare since UnifiedAuthProvider initializes it synchronously
|
|
475
|
+
// Use cached value from updateDependencies as fallback
|
|
476
|
+
// If cached value is true, trust it to avoid blocking super admins
|
|
477
|
+
if (this.isSuperAdmin) {
|
|
478
|
+
userIsSuperAdmin = true;
|
|
479
|
+
logger.warn('EventService', 'RBAC not initialized, using cached super admin status', {
|
|
480
|
+
userId: this.user.id,
|
|
481
|
+
cachedIsSuperAdmin: this.isSuperAdmin,
|
|
482
|
+
note: 'RBAC should be initialized by UnifiedAuthProvider. This may indicate EventService is being used outside the provider.'
|
|
483
|
+
});
|
|
484
|
+
} else {
|
|
485
|
+
logger.warn('EventService', 'RBAC not initialized, using cached non-super-admin status', {
|
|
486
|
+
userId: this.user.id,
|
|
487
|
+
cachedIsSuperAdmin: this.isSuperAdmin,
|
|
488
|
+
note: 'RBAC should be initialized by UnifiedAuthProvider. This may indicate EventService is being used outside the provider.'
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
398
493
|
if (userIsSuperAdmin) {
|
|
399
494
|
// Super admin: Pass null to see all events across all organisations
|
|
400
495
|
organisationIdForRpc = null;
|
|
401
496
|
} else {
|
|
402
|
-
// Not super admin: determine org from context
|
|
497
|
+
// Not super admin: determine org from available context
|
|
498
|
+
// Scope is now page-level only - use whatever context is available
|
|
403
499
|
if (this.selectedEvent) {
|
|
404
500
|
// If event is already selected, use its organisation
|
|
405
501
|
organisationIdForRpc = this.selectedEvent.organisation_id;
|
|
406
|
-
} else if (this.appConfig?.requires_event === true) {
|
|
407
|
-
// Event-required app with no selected event yet: pass null to get all accessible events
|
|
408
|
-
// The RPC will filter by event app roles, returning all events the user has access to
|
|
409
|
-
organisationIdForRpc = null;
|
|
410
502
|
} else if (this.selectedOrganisation) {
|
|
411
|
-
//
|
|
503
|
+
// Use selected organisation
|
|
412
504
|
organisationIdForRpc = this.selectedOrganisation.id;
|
|
413
505
|
} else {
|
|
414
|
-
// No context available -
|
|
415
|
-
|
|
506
|
+
// No context available - pass null to get all accessible events via event-app roles
|
|
507
|
+
// This allows users with event-app roles to see their events even without org context
|
|
508
|
+
logger.debug('EventService', 'No organisation context available, fetching all accessible events', {
|
|
416
509
|
hasSelectedEvent: !!this.selectedEvent,
|
|
417
|
-
hasSelectedOrganisation: !!this.selectedOrganisation
|
|
418
|
-
appRequiresEvent: this.appConfig?.requires_event
|
|
510
|
+
hasSelectedOrganisation: !!this.selectedOrganisation
|
|
419
511
|
});
|
|
420
|
-
organisationIdForRpc = null; // Will return
|
|
512
|
+
organisationIdForRpc = null; // Will return events user has access to via event-app roles
|
|
421
513
|
}
|
|
422
514
|
}
|
|
423
515
|
} catch (superAdminCheckError) {
|
|
424
|
-
// If super admin check fails,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
516
|
+
// If super admin check fails, use cached value as fallback
|
|
517
|
+
// If cached value is true, trust it to avoid blocking super admins
|
|
518
|
+
if (this.isSuperAdmin) {
|
|
519
|
+
userIsSuperAdmin = true;
|
|
520
|
+
organisationIdForRpc = null; // Super admin gets all events
|
|
521
|
+
logger.warn('EventService', 'Super admin check failed, using cached super admin status', {
|
|
522
|
+
error: superAdminCheckError,
|
|
523
|
+
cachedIsSuperAdmin: this.isSuperAdmin
|
|
524
|
+
});
|
|
525
|
+
} else {
|
|
526
|
+
// Fallback: use available context
|
|
527
|
+
logger.warn('EventService', 'Failed to check super admin status, using organisation-scoped query', {
|
|
528
|
+
error: superAdminCheckError,
|
|
529
|
+
cachedIsSuperAdmin: this.isSuperAdmin
|
|
530
|
+
});
|
|
531
|
+
if (this.selectedEvent) {
|
|
532
|
+
organisationIdForRpc = this.selectedEvent.organisation_id;
|
|
533
|
+
} else if (this.selectedOrganisation) {
|
|
534
|
+
organisationIdForRpc = this.selectedOrganisation.id;
|
|
535
|
+
} else {
|
|
536
|
+
// No context - pass null to get all accessible events via event-app roles
|
|
537
|
+
organisationIdForRpc = null;
|
|
538
|
+
}
|
|
436
539
|
}
|
|
437
540
|
}
|
|
438
541
|
|
|
439
542
|
// Call the RPC function following the established pattern
|
|
440
543
|
// For super admins, pass null for p_organisation_id to see all events
|
|
544
|
+
logger.debug('EventService', 'Fetching events', {
|
|
545
|
+
userId: this.user.id,
|
|
546
|
+
organisationIdForRpc,
|
|
547
|
+
appName: this.appName,
|
|
548
|
+
hasSelectedEvent: !!this.selectedEvent,
|
|
549
|
+
hasSelectedOrganisation: !!this.selectedOrganisation,
|
|
550
|
+
isSuperAdmin: userIsSuperAdmin
|
|
551
|
+
});
|
|
552
|
+
|
|
441
553
|
let { data, error: rpcError } = await this.supabaseClient.rpc('data_user_events_get', {
|
|
442
554
|
p_user_id: this.user.id,
|
|
443
555
|
p_organisation_id: organisationIdForRpc,
|
|
444
556
|
p_app_name: this.appName
|
|
445
557
|
});
|
|
446
558
|
|
|
559
|
+
logger.debug('EventService', 'RPC response', {
|
|
560
|
+
dataLength: data?.length || 0,
|
|
561
|
+
hasError: !!rpcError,
|
|
562
|
+
error: rpcError
|
|
563
|
+
});
|
|
564
|
+
|
|
447
565
|
if (rpcError) {
|
|
448
566
|
logger.error('EventService', 'RPC error fetching events:', rpcError);
|
|
449
567
|
throw new Error(rpcError.message || 'Failed to fetch events');
|
|
@@ -482,7 +600,21 @@ export class EventService extends BaseService implements IEventService {
|
|
|
482
600
|
updated_at: new Date().toISOString()
|
|
483
601
|
}));
|
|
484
602
|
|
|
485
|
-
|
|
603
|
+
// Sort events by event_date descending (newest first)
|
|
604
|
+
// Handle null dates by putting them at the end
|
|
605
|
+
const sortedEvents = [...transformedEvents].sort((a, b) => {
|
|
606
|
+
// If both have dates, sort descending (newest first)
|
|
607
|
+
if (a.event_date && b.event_date) {
|
|
608
|
+
return new Date(b.event_date).getTime() - new Date(a.event_date).getTime();
|
|
609
|
+
}
|
|
610
|
+
// If only one has a date, prioritize the one with a date
|
|
611
|
+
if (a.event_date && !b.event_date) return -1;
|
|
612
|
+
if (!a.event_date && b.event_date) return 1;
|
|
613
|
+
// If neither has a date, maintain original order
|
|
614
|
+
return 0;
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
this.events = sortedEvents;
|
|
486
618
|
this.error = null;
|
|
487
619
|
|
|
488
620
|
// Reset auto-selection ref for new events
|
|
@@ -20,7 +20,6 @@ import type {
|
|
|
20
20
|
import { setOrganisationContext } from '../utils/context/organisationContext';
|
|
21
21
|
import { logger } from '../utils/core/logger';
|
|
22
22
|
import { assertUserId, assertOrganisationId } from '../types/core';
|
|
23
|
-
import { isSuperAdmin } from '../rbac/api';
|
|
24
23
|
import type { UUID } from '../rbac/types';
|
|
25
24
|
|
|
26
25
|
// Type for RPC response from data_user_organisation_roles_get
|
|
@@ -42,6 +41,8 @@ interface OrganisationRoleRpcResponse {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
export class OrganisationService extends BaseService implements IOrganisationService {
|
|
44
|
+
private static instanceCount = 0;
|
|
45
|
+
private instanceId: number;
|
|
45
46
|
private _selectedOrganisation: Organisation | null = null;
|
|
46
47
|
private _organisations: Organisation[] = [];
|
|
47
48
|
private _userMemberships: OrganisationMembership[] = [];
|
|
@@ -65,9 +66,18 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
65
66
|
|
|
66
67
|
constructor(supabaseClient: SupabaseClient, user: User | null, session: Session | null) {
|
|
67
68
|
super();
|
|
69
|
+
this.instanceId = ++OrganisationService.instanceCount;
|
|
68
70
|
this.supabaseClient = supabaseClient;
|
|
69
71
|
this.user = user;
|
|
70
72
|
this.session = session;
|
|
73
|
+
logger.debug('OrganisationService', `Instance created [ID:${this.instanceId}]`, {
|
|
74
|
+
hasUser: !!user,
|
|
75
|
+
userId: user?.id
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getInstanceId(): number {
|
|
80
|
+
return this.instanceId;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
// Interface implementation
|
|
@@ -144,23 +154,43 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
144
154
|
|
|
145
155
|
// Update dependencies
|
|
146
156
|
updateDependencies(user: User | null, session: Session | null): void {
|
|
147
|
-
const
|
|
148
|
-
const
|
|
157
|
+
const previousUserId = this.user?.id || null;
|
|
158
|
+
const newUserId = user?.id || null;
|
|
149
159
|
|
|
150
|
-
//
|
|
151
|
-
|
|
160
|
+
// Only reset if the User ID has actually changed (null -> ID or ID -> different ID)
|
|
161
|
+
const userChanged = previousUserId !== newUserId;
|
|
162
|
+
const needsRetry = this._error !== null && !this.isLoadingRef;
|
|
163
|
+
const isEmpty = newUserId !== null && this._organisations.length === 0 && !this.isLoadingRef;
|
|
164
|
+
|
|
165
|
+
if (userChanged || needsRetry || isEmpty) {
|
|
166
|
+
if (userChanged) {
|
|
167
|
+
logger.debug('OrganisationService', `User changed [ID:${this.instanceId}], resetting initialization`, {
|
|
168
|
+
previousUserId,
|
|
169
|
+
newUserId
|
|
170
|
+
});
|
|
171
|
+
} else if (needsRetry) {
|
|
172
|
+
logger.debug('OrganisationService', `Previous error detected [ID:${this.instanceId}], retrying initialization`);
|
|
173
|
+
} else if (isEmpty) {
|
|
174
|
+
logger.debug('OrganisationService', `No organisations found [ID:${this.instanceId}], retrying initialization`);
|
|
175
|
+
}
|
|
176
|
+
|
|
152
177
|
this._isSuperAdmin = false;
|
|
178
|
+
this.resetInitialization();
|
|
179
|
+
|
|
180
|
+
// Only clear all state if the user actually changed
|
|
181
|
+
if (userChanged) {
|
|
182
|
+
this._organisations = [];
|
|
183
|
+
this._userMemberships = [];
|
|
184
|
+
this._roleMapState = new Map();
|
|
185
|
+
this._selectedOrganisation = null;
|
|
186
|
+
this._isContextReady = false;
|
|
187
|
+
}
|
|
188
|
+
this.lastLoadTimeRef = 0; // Allow immediate reload
|
|
153
189
|
}
|
|
154
190
|
|
|
155
191
|
this.user = user;
|
|
156
192
|
this.session = session;
|
|
157
193
|
|
|
158
|
-
// If user logs out, allow re-initialization when they log back in
|
|
159
|
-
if (wasAuthenticated && !isAuthenticated) {
|
|
160
|
-
// Reset BaseService initialization state to allow re-initialization
|
|
161
|
-
this.resetInitialization();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
194
|
this.notify();
|
|
165
195
|
}
|
|
166
196
|
|
|
@@ -260,6 +290,13 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
260
290
|
|
|
261
291
|
// Lifecycle methods
|
|
262
292
|
async initialize(): Promise<void> {
|
|
293
|
+
// SECURITY: Only initialize if we have an authenticated user
|
|
294
|
+
// This prevents premature initialization during early auth states
|
|
295
|
+
if (!this.user) {
|
|
296
|
+
logger.debug('OrganisationService', 'Skipping initialization - no user');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
263
300
|
await super.initialize();
|
|
264
301
|
|
|
265
302
|
// Don't load if already loading (prevents duplicate loads during rapid auth events)
|
|
@@ -351,8 +388,9 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
351
388
|
}
|
|
352
389
|
|
|
353
390
|
// Prevent rapid retries - minimum 2 seconds between attempts
|
|
391
|
+
// Skip this check if we don't have any organisations yet
|
|
354
392
|
const now = Date.now();
|
|
355
|
-
if (now - this.lastLoadTimeRef < 2000) {
|
|
393
|
+
if (this._organisations.length > 0 && now - this.lastLoadTimeRef < 2000) {
|
|
356
394
|
// Ensure loading state is correct
|
|
357
395
|
if (this._organisations.length > 0 || this._selectedOrganisation) {
|
|
358
396
|
this._isLoading = false;
|
|
@@ -377,6 +415,10 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
377
415
|
this._isLoading = true;
|
|
378
416
|
this._error = null;
|
|
379
417
|
this.notify();
|
|
418
|
+
|
|
419
|
+
logger.debug('OrganisationService', 'Loading organisations for user', {
|
|
420
|
+
userId: this.user.id
|
|
421
|
+
});
|
|
380
422
|
|
|
381
423
|
try {
|
|
382
424
|
// Get user's organisation roles directly from rbac_organisation_roles table
|
|
@@ -458,6 +500,11 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
458
500
|
|
|
459
501
|
organisations = Array.from(organisationsMap.values());
|
|
460
502
|
|
|
503
|
+
logger.debug('OrganisationService', 'Query results', {
|
|
504
|
+
membershipsCount: memberships.length,
|
|
505
|
+
organisationsCount: organisations.length
|
|
506
|
+
});
|
|
507
|
+
|
|
461
508
|
// Extract organisations from join results
|
|
462
509
|
} catch (queryError) {
|
|
463
510
|
// Extract error message properly from Supabase error objects
|
|
@@ -476,7 +523,20 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
476
523
|
let userIsSuperAdmin = false;
|
|
477
524
|
if (this.user?.id) {
|
|
478
525
|
try {
|
|
479
|
-
|
|
526
|
+
// Dynamic import to avoid circular dependencies and ensure RBAC is initialized
|
|
527
|
+
const { isSuperAdmin: checkSuperAdmin, isRBACInitialized, setupRBAC } = await import('../rbac/api');
|
|
528
|
+
|
|
529
|
+
// Ensure RBAC is initialized if possible
|
|
530
|
+
if (!isRBACInitialized() && this.supabaseClient) {
|
|
531
|
+
setupRBAC(this.supabaseClient);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (isRBACInitialized()) {
|
|
535
|
+
userIsSuperAdmin = await checkSuperAdmin(this.user.id as UUID);
|
|
536
|
+
} else {
|
|
537
|
+
userIsSuperAdmin = false;
|
|
538
|
+
}
|
|
539
|
+
|
|
480
540
|
this._isSuperAdmin = userIsSuperAdmin; // Cache the result
|
|
481
541
|
} catch (error) {
|
|
482
542
|
logger.warn('OrganisationService', 'Failed to check super admin status', { error });
|
|
@@ -542,7 +602,14 @@ export class OrganisationService extends BaseService implements IOrganisationSer
|
|
|
542
602
|
throw new Error('User has no access to active organisations') as OrganisationSecurityError;
|
|
543
603
|
}
|
|
544
604
|
|
|
545
|
-
|
|
605
|
+
// Sort organisations alphabetically by display_name
|
|
606
|
+
const sortedOrgs = [...activeOrgs].sort((a, b) => {
|
|
607
|
+
const nameA = (a.display_name || a.name || '').toLowerCase();
|
|
608
|
+
const nameB = (b.display_name || b.name || '').toLowerCase();
|
|
609
|
+
return nameA.localeCompare(nameB);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
this._organisations = sortedOrgs;
|
|
546
613
|
// Memberships already have branded types from earlier mapping
|
|
547
614
|
this._userMemberships = memberships as OrganisationMembership[];
|
|
548
615
|
|