From d091b002ba0f87fadfb1c32d763a143bfe755e8d Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Wed, 1 Jul 2026 14:50:44 +1200 Subject: [PATCH] SF-3292 Re-pull auth profile if user not in SF --- .../ClientApp/src/app/app.component.spec.ts | 12 ++++++++++++ .../ClientApp/src/app/app.component.ts | 8 +++++++- .../ClientApp/src/app/join/join.component.ts | 3 ++- .../ClientApp/src/app/join/join.stories.ts | 11 +++++++++++ .../ClientApp/src/assets/i18n/checking_en.json | 1 + .../ClientApp/src/xforge-common/auth.service.ts | 8 ++++++-- .../src/xforge-common/user-projects.service.ts | 2 +- .../Controllers/SFProjectsRpcController.cs | 9 +-------- .../Services/SFProjectService.cs | 5 +++++ .../Services/SFProjectServiceTests.cs | 7 +++++++ 10 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.spec.ts index 49c2741a2d5..87cb5048db9 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.spec.ts @@ -255,6 +255,18 @@ describe('AppComponent', () => { verify(mockedAuthService.updateInterfaceLanguage(anything())).never(); })); + it('pulls the user profile from Auth0 if the user is not present in the realtime service', fakeAsync(() => { + const env = new TestEnvironment(); + when(mockedAuthService.isNewlyLoggedIn).thenResolve(true); + env.setCurrentUser('missing_user_id'); + env.navigate(['/projects', 'project01']); + env.init(); + + tick(); + env.fixture.detectChanges(); + verify(mockedAuthService.pullAuthUserProfile()).once(); + })); + it('sets user locale when stored locale does not match the browsing session', fakeAsync(() => { const env = new TestEnvironment(); when(mockedAuthService.isNewlyLoggedIn).thenResolve(true); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts index 4a7c104f643..b0a3502f80d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts @@ -311,8 +311,14 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest }); } + // If the user is logged in, but they do not have user profile, pull it from Auth0 + const isLoggedIn = await this.authService.isLoggedIn; + if (isLoggedIn && this.currentUserDoc.data == null) { + await this.authService.pullAuthUserProfile(); + } + // Set the locale to the Auth0 user profile on first login - const languageTag: string | undefined = this.currentUserDoc.data!.interfaceLanguage; + const languageTag: string | undefined = this.currentUserDoc.data?.interfaceLanguage; if ( isNewlyLoggedIn && languageTag != null && diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.component.ts index b5594a87f5d..762435700b0 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.component.ts @@ -37,8 +37,9 @@ export const KNOWN_ERROR_CODES: ObjectPaths[] = [ 'key_already_used', 'key_expired', 'max_users_reached', + 'project_link_is_invalid', 'role_not_found', - 'project_link_is_invalid' + 'user_missing' ]; @Component({ diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.stories.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.stories.ts index eae610aa780..01c06773f00 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.stories.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/join/join.stories.ts @@ -31,6 +31,7 @@ enum ShareKeys { InvalidShareKey = 'invalid_share_key', KeyAlreadyUsed = 'key_already_used', MaxUsersReached = 'max_users_reached', + UserMissing = 'user_missing', Valid = 'valid' } @@ -114,6 +115,9 @@ const meta: Meta = { when(mockedSFProjectService.onlineJoinWithShareKey(ShareKeys.InvalidShareKey)).thenThrow( new CommandError(CommandErrorCode.Forbidden, 'project_link_is_invalid') ); + when(mockedSFProjectService.onlineJoinWithShareKey(ShareKeys.UserMissing)).thenThrow( + new CommandError(CommandErrorCode.NotFound, 'user_missing') + ); if (context.args.shareKey === ShareKeys.Valid) { when(mockedAuthService.tryTransparentAuthentication()).thenCall(() => { when(mockedAuthService.isLoggedIn).thenResolve(true); @@ -180,3 +184,10 @@ export const DialogKeyAlreadyUsed: Story = { loggedIn: true } }; + +export const DialogUserMissing: Story = { + args: { + shareKey: ShareKeys.UserMissing, + loggedIn: true + } +}; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json index de5ce5e6594..67ed21ffdbf 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json @@ -380,6 +380,7 @@ "please_connect_to_use_link": "Please connect to the internet to join a project using a link.", "project_link_is_invalid": "The project link is invalid. Please contact the person who sent you the link and ask for a new one.", "role_not_found": "The role you were assigned on this project is no longer available. Please contact the person who sent you the link and ask for a new one.", + "user_missing": "Your user account could not be found. Please log out join using the link again.", "your_name": "Name" }, "dialog": { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/auth.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/auth.service.ts index 4c5a9f02281..ce230028494 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/auth.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/auth.service.ts @@ -283,6 +283,10 @@ export class AuthService { } } + pullAuthUserProfile(): Promise { + return this.commandService.onlineInvoke(USERS_URL, 'pullAuthUserProfile'); + } + requestParatextCredentialUpdate(cancelCallback?: () => void): void { void this.dialogService .confirm('warnings.paratext_credentials_expired', 'warnings.logout') @@ -323,7 +327,7 @@ export class AuthService { this.localSettings.set(cacheKey.toKey(), cacheEntry); await this.localLogIn(authResponse.access_token, authResponse.id_token, authResponse.expires_in); await this.remoteStore.init(() => this.getAccessToken()); - await this.commandService.onlineInvoke(USERS_URL, 'pullAuthUserProfile'); + await this.pullAuthUserProfile(); this._loggedInState$.next({ loggedIn: true, newlyLoggedIn: true, anonymousUser: true }); return true; } @@ -483,7 +487,7 @@ export class AuthService { await this.localLogIn(authDetails.token.access_token, authDetails.token.id_token, authDetails.token.expires_in); await this.remoteStore.init(() => this.getAccessToken()); try { - await this.commandService.onlineInvoke(USERS_URL, 'pullAuthUserProfile'); + await this.pullAuthUserProfile(); } catch (err) { // Display error dialog to pause login loop. // Error details will be sent to Bugsnag and logged to the console. diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts index d77a66e08a2..b830789ad5d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts @@ -53,7 +53,7 @@ export class SFUserProjectsService { /** Updates our provided set of SF project docs for the current user based on the userdoc's list of SF projects the * user is on. */ private async updateProjectList(userDoc: UserDoc): Promise { - const currentProjectIds = userDoc.data!.sites[environment.siteId].projects; + const currentProjectIds: string[] = userDoc.data?.sites[environment.siteId].projects ?? []; let removedProjectsCount = 0; for (const [id, projectDoc] of this._projectDocs) { diff --git a/src/SIL.XForge.Scripture/Controllers/SFProjectsRpcController.cs b/src/SIL.XForge.Scripture/Controllers/SFProjectsRpcController.cs index 5a090f914ce..47eab330658 100644 --- a/src/SIL.XForge.Scripture/Controllers/SFProjectsRpcController.cs +++ b/src/SIL.XForge.Scripture/Controllers/SFProjectsRpcController.cs @@ -474,9 +474,6 @@ public async Task InvitedUsers(string projectId) } } - [Obsolete("Only here for old clients that still call it. Removed 2023-04.")] - public async Task CheckLinkSharing(string shareKey) => Ok(await JoinWithShareKey(shareKey)); - public async Task JoinWithShareKey(string shareKey) { try @@ -494,16 +491,12 @@ public async Task JoinWithShareKey(string shareKey) catch (Exception) { _exceptionHandler.RecordEndpointInfoForException( - new Dictionary { { "method", "CheckLinkSharing" }, { "shareKey", shareKey } } + new Dictionary { { "method", "JoinWithShareKey" }, { "shareKey", shareKey } } ); throw; } } - [Obsolete("New endpoints only require the share key. Old clients would still provide the projectId")] - public async Task CheckLinkSharing(string projectId, string shareKey) => - await CheckLinkSharing(shareKey); - public IRpcMethodResult IsSourceProject(string projectId) => Ok(projectService.IsSourceProject(projectId)); public async Task LinkSharingKey( diff --git a/src/SIL.XForge.Scripture/Services/SFProjectService.cs b/src/SIL.XForge.Scripture/Services/SFProjectService.cs index 7b29781d863..3978b30ddc3 100644 --- a/src/SIL.XForge.Scripture/Services/SFProjectService.cs +++ b/src/SIL.XForge.Scripture/Services/SFProjectService.cs @@ -902,6 +902,11 @@ public async Task JoinWithShareKeyAsync(string curUserId, string shareKe } IDocument userDoc = await conn.FetchAsync(curUserId); + if (!userDoc.IsLoaded) + { + throw new DataNotFoundException("user_missing"); + } + // Attempt to get the role for the user from the Paratext registry Attempt attempt = await TryGetProjectRoleAsync(project, curUserId); if (attempt.TryResult(out string projectRole)) diff --git a/test/SIL.XForge.Scripture.Tests/Services/SFProjectServiceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/SFProjectServiceTests.cs index 8e001091a6f..d856fc44449 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/SFProjectServiceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/SFProjectServiceTests.cs @@ -737,6 +737,13 @@ public void JoinWithShareKeyAsync_LinkSharingDisabledAndUserOnProject_Success() Assert.DoesNotThrowAsync(() => env.Service.JoinWithShareKeyAsync(User02, "abcd")); } + [Test] + public void JoinWithShareKeyAsync_MissingUser() + { + var env = new TestEnvironment(); + Assert.ThrowsAsync(() => env.Service.JoinWithShareKeyAsync("missing_user_id", "abcd")); + } + [Test] public void JoinWithShareKeyAsync_LinkSharingDisabledAndUserNotOnProject_Forbidden() {