Skip to content

Commit

Permalink
Updated SSOAuthentication flow, and PasswordRegistration flow to ensu…
Browse files Browse the repository at this point in the history
…re that they reuse the registered user, when registering the same email address. Closes #77
  • Loading branch information
jezzsantos committed Jan 19, 2025
1 parent 9187717 commit b2bba8c
Show file tree
Hide file tree
Showing 26 changed files with 313 additions and 205 deletions.
5 changes: 5 additions & 0 deletions src/Application.Resources.Shared/EndUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public class EndUser : IIdentifiableResource
public required string Id { get; set; }
}

public class EndUserWithProfile : EndUser
{
public UserProfile? Profile { get; set; }
}

public enum EndUserStatus
{
Unregistered = 0,
Expand Down
2 changes: 1 addition & 1 deletion src/Application.Services.Shared/IEndUsersService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Task<Result<SearchResults<MembershipWithUserProfile>, Error>> ListMembershipsFor
Task<Result<EndUser, Error>> RegisterMachinePrivateAsync(ICallerContext caller, string name,
string? timezone, string? countryCode, CancellationToken cancellationToken);

Task<Result<EndUser, Error>> RegisterPersonPrivateAsync(ICallerContext caller, string? invitationToken,
Task<Result<EndUserWithProfile, Error>> RegisterPersonPrivateAsync(ICallerContext caller, string? invitationToken,
string emailAddress, string firstName, string? lastName, string? timezone, string? countryCode,
bool termsAndConditionsAccepted, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public EndUsersApplicationDomainEventHandlersSpec()
_subscriptionsService = new Mock<ISubscriptionsService>();

_application =
new EndUsersApplication(_recorder.Object, _idFactory.Object, settings.Object, notificationsService.Object,
new EndUsersApplication(_recorder.Object, _idFactory.Object, settings.Object,
_userProfilesService.Object, _subscriptionsService.Object, invitationRepository.Object,
_endUserRepository.Object);
}
Expand Down
56 changes: 21 additions & 35 deletions src/EndUsersApplication.UnitTests/EndUsersApplicationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public class EndUsersApplicationSpec
private readonly Mock<IEndUserRepository> _endUserRepository;
private readonly Mock<IIdentifierFactory> _idFactory;
private readonly Mock<IInvitationRepository> _invitationRepository;
private readonly Mock<IUserNotificationsService> _notificationsService;
private readonly Mock<IRecorder> _recorder;
private readonly Mock<IUserProfilesService> _userProfilesService;

Expand Down Expand Up @@ -64,12 +63,11 @@ public EndUsersApplicationSpec()
.ReturnsAsync((EndUserRoot root, bool _, CancellationToken _) => root);
_invitationRepository = new Mock<IInvitationRepository>();
_userProfilesService = new Mock<IUserProfilesService>();
_notificationsService = new Mock<IUserNotificationsService>();
var subscriptionsService = new Mock<ISubscriptionsService>();

_application =
new EndUsersApplication(_recorder.Object, _idFactory.Object, settings.Object,
_notificationsService.Object, _userProfilesService.Object, subscriptionsService.Object,
new EndUsersApplication(_recorder.Object, _idFactory.Object, settings.Object, _userProfilesService.Object,
subscriptionsService.Object,
_invitationRepository.Object, _endUserRepository.Object);
}

Expand Down Expand Up @@ -147,13 +145,11 @@ public async Task WhenRegisterPersonAsyncAndWasInvitedAsGuest_ThenCompletesRegis
result.Value.Status.Should().Be(EndUserStatus.Registered);
result.Value.Classification.Should().Be(EndUserClassification.Person);
result.Value.Roles.Should().OnlyContain(role => role == PlatformRoles.Standard.Name);
result.Value.Features.Should().ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Features.Should()
.ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Profile.Should().BeNull();
_invitationRepository.Verify(rep =>
rep.FindInvitedGuestByTokenAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
_notificationsService.Verify(
ns => ns.NotifyPasswordRegistrationRepeatCourtesyAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<IReadOnlyList<string>>(), It.IsAny<CancellationToken>()), Times.Never);
}

[Fact]
Expand Down Expand Up @@ -214,13 +210,11 @@ await invitee.InviteGuestAsync(tokensService.Object, "aninviterid".ToId(),
result.Value.Status.Should().Be(EndUserStatus.Registered);
result.Value.Classification.Should().Be(EndUserClassification.Person);
result.Value.Roles.Should().OnlyContain(role => role == PlatformRoles.Standard.Name);
result.Value.Features.Should().ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Features.Should()
.ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Profile.Should().BeNull();
_invitationRepository.Verify(rep =>
rep.FindInvitedGuestByTokenAsync(TestingToken, It.IsAny<CancellationToken>()));
_notificationsService.Verify(
ns => ns.NotifyPasswordRegistrationRepeatCourtesyAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<IReadOnlyList<string>>(), It.IsAny<CancellationToken>()), Times.Never);
}

[Fact]
Expand Down Expand Up @@ -270,13 +264,11 @@ public async Task WhenRegisterPersonAsyncAndAcceptingAnUnknownInvitation_ThenReg
result.Value.Status.Should().Be(EndUserStatus.Registered);
result.Value.Classification.Should().Be(EndUserClassification.Person);
result.Value.Roles.Should().OnlyContain(role => role == PlatformRoles.Standard.Name);
result.Value.Features.Should().ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Features.Should()
.ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Profile.Should().BeNull();
_invitationRepository.Verify(rep =>
rep.FindInvitedGuestByTokenAsync("anunknowninvitationtoken", It.IsAny<CancellationToken>()));
_notificationsService.Verify(
ns => ns.NotifyPasswordRegistrationRepeatCourtesyAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<IReadOnlyList<string>>(), It.IsAny<CancellationToken>()), Times.Never);
}

[Fact]
Expand Down Expand Up @@ -326,14 +318,10 @@ public async Task
ups.GetProfilePrivateAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(),
It.IsAny<CancellationToken>()),
Times.Never);
_notificationsService.Verify(
ns => ns.NotifyPasswordRegistrationRepeatCourtesyAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<IReadOnlyList<string>>(), It.IsAny<CancellationToken>()), Times.Never);
}

[Fact]
public async Task WhenRegisterPersonAsyncAndAlreadyRegistered_ThenSendsCourtesyEmail()
public async Task WhenRegisterPersonAsyncAndAlreadyRegistered_ThenRegisters()
{
var endUser = EndUserRoot.Create(_recorder.Object, _idFactory.Object, UserClassification.Person).Value;
endUser.Register(Roles.Empty, Features.Empty, EndUserProfile.Create("afirstname").Value,
Expand Down Expand Up @@ -364,15 +352,9 @@ public async Task WhenRegisterPersonAsyncAndAlreadyRegistered_ThenSendsCourtesyE
_endUserRepository.Setup(rep =>
rep.LoadAsync(It.IsAny<Identifier>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(endUser);
_notificationsService.Setup(ns =>
ns.NotifyPasswordRegistrationRepeatCourtesyAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<IReadOnlyList<string>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Ok);

var result = await _application.RegisterPersonAsync(_caller.Object, null, "[email protected]",
"afirstname",
"alastname", null, null, true, CancellationToken.None);
"afirstname", "alastname", null, null, true, CancellationToken.None);

result.Should().BeSuccess();
result.Value.Id.Should().Be("anid");
Expand All @@ -381,15 +363,17 @@ public async Task WhenRegisterPersonAsyncAndAlreadyRegistered_ThenSendsCourtesyE
result.Value.Classification.Should().Be(EndUserClassification.Person);
result.Value.Roles.Should().BeEmpty();
result.Value.Features.Should().BeEmpty();
result.Value.Profile!.EmailAddress.Should().Be("[email protected]");
result.Value.Profile.Name.FirstName.Should().Be("afirstname");
result.Value.Profile.Name.LastName.Should().Be("alastname");
result.Value.Profile.Timezone.Should().Be("atimezone");
result.Value.Profile.Address.CountryCode.Should().Be("acountrycode");
_invitationRepository.Verify(rep =>
rep.FindInvitedGuestByTokenAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
_userProfilesService.Verify(ups =>
ups.GetProfilePrivateAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(),
It.IsAny<CancellationToken>()),
Times.Never);
_notificationsService.Verify(ns => ns.NotifyPasswordRegistrationRepeatCourtesyAsync(_caller.Object, "anid",
"[email protected]", "afirstname", "atimezone", "acountrycode",
UserNotificationConstants.EmailTags.RegistrationRepeatCourtesy, CancellationToken.None));
}

[Fact]
Expand Down Expand Up @@ -436,7 +420,9 @@ public async Task WhenRegisterPersonAsyncAndNeverRegisteredNorInvitedAsGuest_The
result.Value.Status.Should().Be(EndUserStatus.Registered);
result.Value.Classification.Should().Be(EndUserClassification.Person);
result.Value.Roles.Should().OnlyContain(role => role == PlatformRoles.Standard.Name);
result.Value.Features.Should().ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Features.Should()
.ContainInOrder(PlatformFeatures.PaidTrial.Name, PlatformFeatures.Basic.Name);
result.Value.Profile.Should().BeNull();
_invitationRepository.Verify(rep =>
rep.FindInvitedGuestByTokenAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
Expand Down
60 changes: 24 additions & 36 deletions src/EndUsersApplication/EndUsersApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,15 @@ public partial class EndUsersApplication : IEndUsersApplication
private readonly IRecorder _recorder;
private readonly IConfigurationSettings _settings;
private readonly ISubscriptionsService _subscriptionsService;
private readonly IUserNotificationsService _userNotificationsService;
private readonly IUserProfilesService _userProfilesService;

public EndUsersApplication(IRecorder recorder, IIdentifierFactory idFactory, IConfigurationSettings settings,
IUserNotificationsService userNotificationsService, IUserProfilesService userProfilesService,
ISubscriptionsService subscriptionsService,
IUserProfilesService userProfilesService, ISubscriptionsService subscriptionsService,
IInvitationRepository invitationRepository, IEndUserRepository endUserRepository)
{
_recorder = recorder;
_idFactory = idFactory;
_settings = settings;
_userNotificationsService = userNotificationsService;
_userProfilesService = userProfilesService;
_subscriptionsService = subscriptionsService;
_invitationRepository = invitationRepository;
Expand Down Expand Up @@ -295,7 +292,7 @@ public async Task<Result<EndUser, Error>> RegisterMachineAsync(ICallerContext ca
return machine.ToUser();
}

public async Task<Result<EndUser, Error>> RegisterPersonAsync(ICallerContext caller,
public async Task<Result<EndUserWithProfile, Error>> RegisterPersonAsync(ICallerContext caller,
string? invitationToken, string emailAddress, string firstName, string? lastName, string? timezone,
string? countryCode, bool termsAndConditionsAccepted, CancellationToken cancellationToken)
{
Expand All @@ -312,7 +309,7 @@ public async Task<Result<EndUser, Error>> RegisterPersonAsync(ICallerContext cal

var username = email.Value;

var existingUser = Optional<EndUserWithProfile>.None;
var existingUser = Optional<UserAndProfile>.None;
if (invitationToken.HasValue())
{
var retrievedGuest =
Expand Down Expand Up @@ -345,7 +342,7 @@ public async Task<Result<EndUser, Error>> RegisterPersonAsync(ICallerContext cal
}

_recorder.TraceInformation(caller.ToCall(), "Guest user {Id} accepted their invitation", invitee.Id);
existingUser = new EndUserWithProfile(invitee, null);
existingUser = new UserAndProfile(invitee, null);
}
}

Expand Down Expand Up @@ -376,25 +373,7 @@ public async Task<Result<EndUser, Error>> RegisterPersonAsync(ICallerContext cal
return Error.EntityNotFound(Resources.EndUsersApplication_NotPersonProfile);
}

var notified = await _userNotificationsService.NotifyPasswordRegistrationRepeatCourtesyAsync(caller,
unregisteredUser.Id, unregisteredUserProfile.EmailAddress, unregisteredUserProfile.DisplayName,
unregisteredUserProfile.Timezone, unregisteredUserProfile.Address.CountryCode,
UserNotificationConstants.EmailTags.RegistrationRepeatCourtesy, cancellationToken);
if (notified.IsFailure)
{
return notified.Error;
}

_recorder.TraceInformation(caller.ToCall(),
"Attempted re-registration of user: {Id}, with email {EmailAddress}", unregisteredUser.Id, email);
_recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.PersonReRegistered,
new Dictionary<string, object>
{
{ UsageConstants.Properties.Id, unregisteredUser.Id },
{ UsageConstants.Properties.EmailAddress, username.Address }
});

return unregisteredUser.ToUser();
return unregisteredUser.ToUserWithUserProfile(unregisteredUserProfile);
}
}
else
Expand Down Expand Up @@ -444,7 +423,7 @@ public async Task<Result<EndUser, Error>> RegisterPersonAsync(ICallerContext cal
{ UsageConstants.Properties.UserIdOverride, person.Id }
});

return person.ToUser();
return person.ToUserWithUserProfile(null);
}

public async Task<Result<EndUser, Error>> UnassignPlatformRolesAsync(ICallerContext caller, string id,
Expand Down Expand Up @@ -538,7 +517,7 @@ private static bool IsMember(Identifier userId, List<MembershipJoinInvitation> m
return members.Any(ms => ms.UserId.Value.EqualsIgnoreCase(userId));
}

private async Task<Result<Optional<EndUserWithProfile>, Error>>
private async Task<Result<Optional<UserAndProfile>, Error>>
FindRegisteredPersonOrInvitedGuestByEmailAddressAsync(ICallerContext caller, EmailAddress emailAddress,
CancellationToken cancellationToken)
{
Expand All @@ -564,10 +543,10 @@ private async Task<Result<Optional<EndUserWithProfile>, Error>>
return existingInvitation;
}

return Optional<EndUserWithProfile>.None;
return Optional<UserAndProfile>.None;
}

private async Task<Result<Optional<EndUserWithProfile>, Error>> FindProfileWithEmailAddressAsync(
private async Task<Result<Optional<UserAndProfile>, Error>> FindProfileWithEmailAddressAsync(
ICallerContext caller, EmailAddress emailAddress, CancellationToken cancellationToken)
{
var retrievedProfile =
Expand All @@ -587,10 +566,10 @@ await _userProfilesService.FindPersonByEmailAddressPrivateAsync(caller, emailAdd
return user.Error;
}

return new EndUserWithProfile(user.Value, profile).ToOptional();
return new UserAndProfile(user.Value, profile).ToOptional();
}

return Optional<EndUserWithProfile>.None;
return Optional<UserAndProfile>.None;
}

private async Task<Result<UserProfile, Error>> GetUserProfileAsync(ICallerContext caller, Identifier userId,
Expand All @@ -600,7 +579,7 @@ private async Task<Result<UserProfile, Error>> GetUserProfileAsync(ICallerContex
return await _userProfilesService.GetProfilePrivateAsync(maintenance, userId, cancellationToken);
}

private async Task<Result<Optional<EndUserWithProfile>, Error>> FindInvitedGuestWithEmailAddressAsync(
private async Task<Result<Optional<UserAndProfile>, Error>> FindInvitedGuestWithEmailAddressAsync(
EmailAddress emailAddress, CancellationToken cancellationToken)
{
var invitedGuest =
Expand All @@ -611,8 +590,8 @@ private async Task<Result<Optional<EndUserWithProfile>, Error>> FindInvitedGuest
}

return invitedGuest.Value.HasValue
? new EndUserWithProfile(invitedGuest.Value, null).ToOptional()
: Optional<EndUserWithProfile>.None;
? new UserAndProfile(invitedGuest.Value, null).ToOptional()
: Optional<UserAndProfile>.None;
}

private async Task<Result<Optional<EndUserRoot>, Error>> FindInvitedGuestWithInvitationTokenAsync(
Expand Down Expand Up @@ -646,7 +625,7 @@ private Optional<List<EmailAddress>> GetPermittedOperators()
.ToList()!;
}

private record EndUserWithProfile(EndUserRoot User, UserProfile? Profile);
private record UserAndProfile(EndUserRoot User, UserProfile? Profile);
}

internal static class EndUserConversionExtensions
Expand Down Expand Up @@ -758,4 +737,13 @@ public static EndUserWithMemberships ToUserWithMemberships(this EndUserRoot user

return withMemberships;
}

public static EndUserWithProfile ToUserWithUserProfile(this EndUserRoot user, UserProfile? profile)
{
var endUser = ToUser(user);
var withProfile = endUser.Convert<EndUser, EndUserWithProfile>();
withProfile.Profile = profile;

return withProfile;
}
}
2 changes: 1 addition & 1 deletion src/EndUsersApplication/IEndUsersApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Task<Result<SearchResults<MembershipWithUserProfile>, Error>> ListMembershipsFor
Task<Result<EndUser, Error>> RegisterMachineAsync(ICallerContext caller, string name, string? timezone,
string? countryCode, CancellationToken cancellationToken);

Task<Result<EndUser, Error>> RegisterPersonAsync(ICallerContext caller, string? invitationToken,
Task<Result<EndUserWithProfile, Error>> RegisterPersonAsync(ICallerContext caller, string? invitationToken,
string emailAddress,
string firstName, string? lastName, string? timezone, string? countryCode, bool termsAndConditionsAccepted,
CancellationToken cancellationToken);
Expand Down
9 changes: 0 additions & 9 deletions src/EndUsersApplication/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions src/EndUsersApplication/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@
<data name="EndUsersApplication_NotAcceptedTerms" xml:space="preserve">
<value>This user must accept the terms of service</value>
</data>
<data name="EndUsersApplication_MembershipNotFound" xml:space="preserve">
<value>The membership could not be found</value>
</data>
<data name="EndUsersApplication_NotPersonProfile" xml:space="preserve">
<value>The profile for this person cannot be found</value>
</data>
Expand Down
Loading

0 comments on commit b2bba8c

Please sign in to comment.