Skip to content

Commit c2631c7

Browse files
authored
SF-3292 Re-pull auth profile if user not in SF (#3979)
1 parent 201299b commit c2631c7

10 files changed

Lines changed: 54 additions & 13 deletions

File tree

src/SIL.XForge.Scripture/ClientApp/src/app/app.component.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ describe('AppComponent', () => {
255255
verify(mockedAuthService.updateInterfaceLanguage(anything())).never();
256256
}));
257257

258+
it('pulls the user profile from Auth0 if the user is not present in the realtime service', fakeAsync(() => {
259+
const env = new TestEnvironment();
260+
when(mockedAuthService.isNewlyLoggedIn).thenResolve(true);
261+
env.setCurrentUser('missing_user_id');
262+
env.navigate(['/projects', 'project01']);
263+
env.init();
264+
265+
tick();
266+
env.fixture.detectChanges();
267+
verify(mockedAuthService.pullAuthUserProfile()).once();
268+
}));
269+
258270
it('sets user locale when stored locale does not match the browsing session', fakeAsync(() => {
259271
const env = new TestEnvironment();
260272
when(mockedAuthService.isNewlyLoggedIn).thenResolve(true);

src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,15 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
311311
});
312312
}
313313

314+
// If the user is logged in, but they do not have user profile, pull it from Auth0
315+
const isLoggedIn = await this.authService.isLoggedIn;
316+
if (isLoggedIn && this.currentUserDoc.data == null) {
317+
await this.authService.pullAuthUserProfile();
318+
await this.currentUserDoc.onlineFetch();
319+
}
320+
314321
// Set the locale to the Auth0 user profile on first login
315-
const languageTag: string | undefined = this.currentUserDoc.data!.interfaceLanguage;
322+
const languageTag: string | undefined = this.currentUserDoc.data?.interfaceLanguage;
316323
if (
317324
isNewlyLoggedIn &&
318325
languageTag != null &&

src/SIL.XForge.Scripture/ClientApp/src/app/join/join.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ export const KNOWN_ERROR_CODES: ObjectPaths<typeof en.join>[] = [
3737
'key_already_used',
3838
'key_expired',
3939
'max_users_reached',
40+
'project_link_is_invalid',
4041
'role_not_found',
41-
'project_link_is_invalid'
42+
'user_missing'
4243
];
4344

4445
@Component({

src/SIL.XForge.Scripture/ClientApp/src/app/join/join.stories.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ enum ShareKeys {
3131
InvalidShareKey = 'invalid_share_key',
3232
KeyAlreadyUsed = 'key_already_used',
3333
MaxUsersReached = 'max_users_reached',
34+
UserMissing = 'user_missing',
3435
Valid = 'valid'
3536
}
3637

@@ -114,6 +115,9 @@ const meta: Meta = {
114115
when(mockedSFProjectService.onlineJoinWithShareKey(ShareKeys.InvalidShareKey)).thenThrow(
115116
new CommandError(CommandErrorCode.Forbidden, 'project_link_is_invalid')
116117
);
118+
when(mockedSFProjectService.onlineJoinWithShareKey(ShareKeys.UserMissing)).thenThrow(
119+
new CommandError(CommandErrorCode.NotFound, 'user_missing')
120+
);
117121
if (context.args.shareKey === ShareKeys.Valid) {
118122
when(mockedAuthService.tryTransparentAuthentication()).thenCall(() => {
119123
when(mockedAuthService.isLoggedIn).thenResolve(true);
@@ -180,3 +184,10 @@ export const DialogKeyAlreadyUsed: Story = {
180184
loggedIn: true
181185
}
182186
};
187+
188+
export const DialogUserMissing: Story = {
189+
args: {
190+
shareKey: ShareKeys.UserMissing,
191+
loggedIn: true
192+
}
193+
};

src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@
382382
"please_connect_to_use_link": "Please connect to the internet to join a project using a link.",
383383
"project_link_is_invalid": "The project link is invalid. Please contact the person who sent you the link and ask for a new one.",
384384
"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.",
385+
"user_missing": "Your user account could not be found. Please log out and join using the link again.",
385386
"your_name": "Name"
386387
},
387388
"dialog": {

src/SIL.XForge.Scripture/ClientApp/src/xforge-common/auth.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ export class AuthService {
283283
}
284284
}
285285

286+
pullAuthUserProfile(): Promise<void> {
287+
return this.commandService.onlineInvoke(USERS_URL, 'pullAuthUserProfile');
288+
}
289+
286290
requestParatextCredentialUpdate(cancelCallback?: () => void): void {
287291
void this.dialogService
288292
.confirm('warnings.paratext_credentials_expired', 'warnings.logout')
@@ -323,7 +327,7 @@ export class AuthService {
323327
this.localSettings.set(cacheKey.toKey(), cacheEntry);
324328
await this.localLogIn(authResponse.access_token, authResponse.id_token, authResponse.expires_in);
325329
await this.remoteStore.init(() => this.getAccessToken());
326-
await this.commandService.onlineInvoke(USERS_URL, 'pullAuthUserProfile');
330+
await this.pullAuthUserProfile();
327331
this._loggedInState$.next({ loggedIn: true, newlyLoggedIn: true, anonymousUser: true });
328332
return true;
329333
}
@@ -483,7 +487,7 @@ export class AuthService {
483487
await this.localLogIn(authDetails.token.access_token, authDetails.token.id_token, authDetails.token.expires_in);
484488
await this.remoteStore.init(() => this.getAccessToken());
485489
try {
486-
await this.commandService.onlineInvoke(USERS_URL, 'pullAuthUserProfile');
490+
await this.pullAuthUserProfile();
487491
} catch (err) {
488492
// Display error dialog to pause login loop.
489493
// Error details will be sent to Bugsnag and logged to the console.

src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class SFUserProjectsService {
5353
/** Updates our provided set of SF project docs for the current user based on the userdoc's list of SF projects the
5454
* user is on. */
5555
private async updateProjectList(userDoc: UserDoc): Promise<void> {
56-
const currentProjectIds = userDoc.data!.sites[environment.siteId].projects;
56+
const currentProjectIds: string[] = userDoc.data?.sites[environment.siteId].projects ?? [];
5757

5858
let removedProjectsCount = 0;
5959
for (const [id, projectDoc] of this._projectDocs) {

src/SIL.XForge.Scripture/Controllers/SFProjectsRpcController.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,6 @@ public async Task<IRpcMethodResult> InvitedUsers(string projectId)
474474
}
475475
}
476476

477-
[Obsolete("Only here for old clients that still call it. Removed 2023-04.")]
478-
public async Task<IRpcMethodResult> CheckLinkSharing(string shareKey) => Ok(await JoinWithShareKey(shareKey));
479-
480477
public async Task<IRpcMethodResult> JoinWithShareKey(string shareKey)
481478
{
482479
try
@@ -494,16 +491,12 @@ public async Task<IRpcMethodResult> JoinWithShareKey(string shareKey)
494491
catch (Exception)
495492
{
496493
_exceptionHandler.RecordEndpointInfoForException(
497-
new Dictionary<string, string> { { "method", "CheckLinkSharing" }, { "shareKey", shareKey } }
494+
new Dictionary<string, string> { { "method", "JoinWithShareKey" }, { "shareKey", shareKey } }
498495
);
499496
throw;
500497
}
501498
}
502499

503-
[Obsolete("New endpoints only require the share key. Old clients would still provide the projectId")]
504-
public async Task<IRpcMethodResult> CheckLinkSharing(string projectId, string shareKey) =>
505-
await CheckLinkSharing(shareKey);
506-
507500
public IRpcMethodResult IsSourceProject(string projectId) => Ok(projectService.IsSourceProject(projectId));
508501

509502
public async Task<IRpcMethodResult> LinkSharingKey(

src/SIL.XForge.Scripture/Services/SFProjectService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,11 @@ public async Task<string> JoinWithShareKeyAsync(string curUserId, string shareKe
902902
}
903903

904904
IDocument<User> userDoc = await conn.FetchAsync<User>(curUserId);
905+
if (!userDoc.IsLoaded)
906+
{
907+
throw new DataNotFoundException("user_missing");
908+
}
909+
905910
// Attempt to get the role for the user from the Paratext registry
906911
Attempt<string> attempt = await TryGetProjectRoleAsync(project, curUserId);
907912
if (attempt.TryResult(out string projectRole))

test/SIL.XForge.Scripture.Tests/Services/SFProjectServiceTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,13 @@ public void JoinWithShareKeyAsync_LinkSharingDisabledAndUserOnProject_Success()
737737
Assert.DoesNotThrowAsync(() => env.Service.JoinWithShareKeyAsync(User02, "abcd"));
738738
}
739739

740+
[Test]
741+
public void JoinWithShareKeyAsync_MissingUser()
742+
{
743+
var env = new TestEnvironment();
744+
Assert.ThrowsAsync<DataNotFoundException>(() => env.Service.JoinWithShareKeyAsync("missing_user_id", "abcd"));
745+
}
746+
740747
[Test]
741748
public void JoinWithShareKeyAsync_LinkSharingDisabledAndUserNotOnProject_Forbidden()
742749
{

0 commit comments

Comments
 (0)