Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
Implementing profile phone.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed May 1, 2024
1 parent d1bf49e commit d5454e6
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ private async Task<SignInCommandResult> CompleteProfileAsync(CompleteProfilePayl
}
Guid userId = Guid.Parse(validatedToken.Subject);
User user = await _userService.FindAsync(userId, cancellationToken) ?? throw new InvalidOperationException($"The user 'Id={userId}' could not be found.");
user = await _userService.SaveProfileAsync(user.Id, payload, cancellationToken);
Phone? phone = validatedToken.GetPhone();
user = await _userService.CompleteProfileAsync(user.Id, payload, phone, cancellationToken);

return await EnsureProfileIsCompleted(user, customAttributes, cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface IUserService
{
Task<User> AuthenticateAsync(User user, string password, CancellationToken cancellationToken = default);
Task<User> AuthenticateAsync(string uniqueName, string password, CancellationToken cancellationToken = default);
Task<User> CompleteProfileAsync(Guid userId, CompleteProfilePayload payload, Phone? phone = null, CancellationToken cancellationToken = default);
Task<User> CreateAsync(Email email, CancellationToken cancellationToken = default);
Task<User?> FindAsync(string uniqueName, CancellationToken cancellationToken = default);
Task<User?> FindAsync(Guid id, CancellationToken cancellationToken = default);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using Logitar.Identity.Domain.Users;
using Logitar.Identity.Contracts;
using Logitar.Identity.Contracts.Users;
using Logitar.Identity.Domain.Users;
using Logitar.Master.Application.Constants;
using Logitar.Master.Contracts.Accounts;
using Logitar.Portal.Contracts;
using Logitar.Portal.Contracts.Tokens;
using Logitar.Portal.Contracts.Users;
using Logitar.Security.Claims;

namespace Logitar.Master.Application.Accounts;

Expand All @@ -20,6 +25,43 @@ public static void SetMultiFactorAuthenticationMode(this UpdateUserPayload paylo
payload.CustomAttributes.Add(new CustomAttributeModification(MultiFactorAuthenticationModeKey, mode.ToString()));
}

public static Phone? GetPhone(this ValidatedToken validatedToken) // TODO(fpion): unit tests
{
Phone phone = new();
foreach (TokenClaim claim in validatedToken.Claims)
{
switch (claim.Name)
{
case ClaimNames.PhoneCountryCode:
phone.CountryCode = claim.Value;
break;
case ClaimNames.PhoneNumberRaw:
phone.Number = claim.Value;
break;
case Rfc7519ClaimNames.IsPhoneVerified:
phone.IsVerified = bool.Parse(claim.Value);
break;
case Rfc7519ClaimNames.PhoneNumber:
int index = claim.Value.IndexOf(';');
if (index < 0)
{
phone.E164Formatted = claim.Value;
}
else
{
phone.E164Formatted = claim.Value[..index];
phone.Extension = claim.Value[(index + 1)..].Remove("ext=");
}
break;
}
}
if (string.IsNullOrEmpty(phone.Number) || string.IsNullOrEmpty(phone.E164Formatted))
{
return null;
}
return phone;
}

public static string GetSubject(this User user) => user.Id.ToString();

public static void CompleteProfile(this UpdateUserPayload payload)
Expand All @@ -37,13 +79,30 @@ public static bool IsProfileCompleted(this User user)
return customAttribute == null ? null : DateTime.Parse(customAttribute.Value);
}

public static EmailPayload ToEmailPayload(this Email email) => email.ToEmailPayload(email.IsVerified); // TODO(fpion): unit tests
public static EmailPayload ToEmailPayload(this IEmail email, bool isVerified = false) => new(email.Address, isVerified); // TODO(fpion): unit tests

public static Phone ToPhone(this AccountPhone phone)
{
Phone result = new(phone.CountryCode?.CleanTrim(), phone.Number.Trim(), extension: null, e164Formatted: string.Empty);
result.E164Formatted = result.FormatToE164();
return result;
}

public static PhonePayload ToPhonePayload(this Phone phone) => phone.ToPhonePayload(phone.IsVerified); // TODO(fpion): unit tests
public static PhonePayload ToPhonePayload(this IPhone phone, bool isVerified = false) => new(phone.CountryCode, phone.Number, phone.Extension, isVerified); // TODO(fpion): unit tests

public static UpdateUserPayload ToUpdateUserPayload(this SaveProfilePayload payload) => new()
{
FirstName = new Modification<string>(payload.FirstName),
MiddleName = new Modification<string>(payload.MiddleName),
LastName = new Modification<string>(payload.LastName),
Birthdate = new Modification<DateTime?>(payload.Birthdate),
Gender = new Modification<string>(payload.Gender),
Locale = new Modification<string>(payload.Locale),
TimeZone = new Modification<string>(payload.TimeZone)
}; // TODO(fpion): unit tests

public static UserProfile ToUserProfile(this User user) => new()
{
CreatedOn = user.CreatedOn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ public record CompleteProfilePayload : SaveProfilePayload
public string? Password { get; set; }
public MultiFactorAuthenticationMode MultiFactorAuthenticationMode { get; set; }

public string? PhoneNumber { get; set; }

public CompleteProfilePayload() : this(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty)
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ public async Task<User> AuthenticateAsync(string uniqueName, string password, Ca
return await _userClient.AuthenticateAsync(payload, context);
}

public async Task<User> CompleteProfileAsync(Guid userId, CompleteProfilePayload profile, Phone? phone, CancellationToken cancellationToken)
{
UpdateUserPayload payload = profile.ToUpdateUserPayload();
if (profile.Password != null)
{
payload.Password = new ChangePasswordPayload(profile.Password);
}
if (phone != null)
{
payload.Phone = new Modification<PhonePayload>(phone.ToPhonePayload());
}
payload.CompleteProfile();
payload.SetMultiFactorAuthenticationMode(profile.MultiFactorAuthenticationMode);
RequestContext context = new(userId.ToString(), cancellationToken);
return await _userClient.UpdateAsync(userId, payload, context) ?? throw new InvalidOperationException($"The user 'Id={userId}' could not be found.");
}

public async Task<User> CreateAsync(Email email, CancellationToken cancellationToken)
{
CreateUserPayload payload = new(email.Address)
Expand All @@ -50,31 +67,7 @@ public async Task<User> CreateAsync(Email email, CancellationToken cancellationT

public async Task<User> SaveProfileAsync(Guid userId, SaveProfilePayload profile, CancellationToken cancellationToken)
{
UpdateUserPayload payload = new()
{
FirstName = new Modification<string>(profile.FirstName),
MiddleName = new Modification<string>(profile.MiddleName),
LastName = new Modification<string>(profile.LastName),
Birthdate = new Modification<DateTime?>(profile.Birthdate),
Gender = new Modification<string>(profile.Gender),
Locale = new Modification<string>(profile.Locale),
TimeZone = new Modification<string>(profile.TimeZone)
};

if (profile is CompleteProfilePayload completedProfile)
{
if (completedProfile.Password != null)
{
payload.Password = new ChangePasswordPayload(completedProfile.Password);
}
if (completedProfile.PhoneNumber != null)
{
payload.Phone = new Modification<PhonePayload>(new PhonePayload(countryCode: null, completedProfile.PhoneNumber, extension: null, isVerified: false));
}
payload.CompleteProfile();
payload.SetMultiFactorAuthenticationMode(completedProfile.MultiFactorAuthenticationMode);
}

UpdateUserPayload payload = profile.ToUpdateUserPayload();
RequestContext context = new(userId.ToString(), cancellationToken);
return await _userClient.UpdateAsync(userId, payload, context) ?? throw new InvalidOperationException($"The user 'Id={userId}' could not be found.");
}
Expand All @@ -83,7 +76,7 @@ public async Task<User> UpdateEmailAsync(Guid userId, Email email, CancellationT
{
UpdateUserPayload payload = new()
{
Email = new Modification<EmailPayload>(new EmailPayload(email.Address, email.IsVerified))
Email = new Modification<EmailPayload>(email.ToEmailPayload())
};
RequestContext context = new(userId.ToString(), cancellationToken);
return await _userClient.UpdateAsync(userId, payload, context) ?? throw new InvalidOperationException($"The user 'Id={userId}' could not be found.");
Expand All @@ -93,7 +86,7 @@ public async Task<User> UpdatePhoneAsync(Guid userId, Phone phone, CancellationT
{
UpdateUserPayload payload = new()
{
Phone = new Modification<PhonePayload>(new PhonePayload(phone.CountryCode, phone.Number, phone.Extension, phone.IsVerified))
Phone = new Modification<PhonePayload>(phone.ToPhonePayload())
};
RequestContext context = new(userId.ToString(), cancellationToken);
return await _userClient.UpdateAsync(userId, payload, context) ?? throw new InvalidOperationException($"The user 'Id={userId}' could not be found.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Logitar.Master.Contracts.Accounts;
using Logitar.Master.Application.Constants;
using Logitar.Master.Contracts.Accounts;
using Logitar.Portal.Contracts;
using Logitar.Portal.Contracts.Messages;
using Logitar.Portal.Contracts.Passwords;
using Logitar.Portal.Contracts.Sessions;
using Logitar.Portal.Contracts.Tokens;
using Logitar.Portal.Contracts.Users;
using Logitar.Security.Claims;
using Moq;

namespace Logitar.Master.Application.Accounts.Commands;
Expand All @@ -25,7 +27,6 @@ public async Task It_should_complete_the_user_profile()
{
Password = "P@s$W0rD",
MultiFactorAuthenticationMode = MultiFactorAuthenticationMode.Phone,
PhoneNumber = "(514) 845-4636",
Birthdate = Faker.Person.DateOfBirth,
Gender = Faker.Person.Gender.ToString().ToLower()
}
Expand All @@ -45,7 +46,10 @@ public async Task It_should_complete_the_user_profile()
{
HasPassword = true,
Email = user.Email,
Phone = new Phone(countryCode: "CA", "(514) 845-4636", extension: null, e164Formatted: "+15148454636"),
Phone = new Phone(countryCode: "CA", "(514) 845-4636", extension: null, e164Formatted: "+15148454636")
{
IsVerified = true
},
FirstName = Faker.Person.FirstName,
LastName = Faker.Person.LastName,
FullName = Faker.Person.FullName,
Expand All @@ -56,12 +60,17 @@ public async Task It_should_complete_the_user_profile()
};
updatedUser.CustomAttributes.Add(new CustomAttribute("MultiFactorAuthenticationMode", MultiFactorAuthenticationMode.Phone.ToString()));
updatedUser.CustomAttributes.Add(new CustomAttribute("ProfileCompletedOn", DateTime.Now.ToString("O", CultureInfo.InvariantCulture)));
UserService.Setup(x => x.SaveProfileAsync(user.Id, payload.Profile, CancellationToken)).ReturnsAsync(updatedUser);
UserService.Setup(x => x.CompleteProfileAsync(user.Id, payload.Profile, updatedUser.Phone, CancellationToken)).ReturnsAsync(updatedUser);

ValidatedToken validatedToken = new()
{
Subject = user.Id.ToString()
};
Assert.NotNull(updatedUser.Phone.CountryCode);
validatedToken.Claims.Add(new TokenClaim(ClaimNames.PhoneCountryCode, updatedUser.Phone.CountryCode));
validatedToken.Claims.Add(new TokenClaim(ClaimNames.PhoneNumberRaw, updatedUser.Phone.Number));
validatedToken.Claims.Add(new TokenClaim(Rfc7519ClaimNames.PhoneNumber, updatedUser.Phone.E164Formatted));
validatedToken.Claims.Add(new TokenClaim(Rfc7519ClaimNames.IsPhoneVerified, "true"));
TokenService.Setup(x => x.ValidateAsync(payload.Profile.Token, "profile+jwt", CancellationToken)).ReturnsAsync(validatedToken);

Session session = new(updatedUser);
Expand Down

0 comments on commit d5454e6

Please sign in to comment.