This repository has been archived by the owner on Jul 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implemented password recovery. * Completed password reset.
- Loading branch information
Showing
19 changed files
with
438 additions
and
2 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
backend/src/Logitar.Master.Application/Accounts/Commands/ResetPasswordCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
using Logitar.Master.Contracts.Accounts; | ||
using Logitar.Portal.Contracts; | ||
using MediatR; | ||
|
||
namespace Logitar.Master.Application.Accounts.Commands; | ||
|
||
public record ResetPasswordCommand(ResetPasswordPayload Payload, IEnumerable<CustomAttribute> CustomAttributes) : IRequest<ResetPasswordResult>; |
104 changes: 104 additions & 0 deletions
104
backend/src/Logitar.Master.Application/Accounts/Commands/ResetPasswordCommandHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
using FluentValidation; | ||
using Logitar.Master.Application.Accounts.Events; | ||
using Logitar.Master.Application.Accounts.Validators; | ||
using Logitar.Master.Application.Constants; | ||
using Logitar.Master.Contracts.Accounts; | ||
using Logitar.Portal.Contracts; | ||
using Logitar.Portal.Contracts.Messages; | ||
using Logitar.Portal.Contracts.Realms; | ||
using Logitar.Portal.Contracts.Sessions; | ||
using Logitar.Portal.Contracts.Tokens; | ||
using Logitar.Portal.Contracts.Users; | ||
using MediatR; | ||
|
||
namespace Logitar.Master.Application.Accounts.Commands; | ||
|
||
internal class ResetPasswordCommandHandler : IRequestHandler<ResetPasswordCommand, ResetPasswordResult> | ||
{ | ||
private readonly IMessageService _messageService; | ||
private readonly IPublisher _publisher; | ||
private readonly IRealmService _realmService; | ||
private readonly ISessionService _sessionService; | ||
private readonly ITokenService _tokenService; | ||
private readonly IUserService _userService; | ||
|
||
public ResetPasswordCommandHandler(IMessageService messageService, IPublisher publisher, | ||
IRealmService realmService, ISessionService sessionService, ITokenService tokenService, IUserService userService) | ||
{ | ||
_messageService = messageService; | ||
_publisher = publisher; | ||
_realmService = realmService; | ||
_sessionService = sessionService; | ||
_tokenService = tokenService; | ||
_userService = userService; | ||
} | ||
|
||
public async Task<ResetPasswordResult> Handle(ResetPasswordCommand command, CancellationToken cancellationToken) | ||
{ | ||
Realm realm = await _realmService.FindAsync(cancellationToken); | ||
|
||
ResetPasswordPayload payload = command.Payload; | ||
new ResetPasswordValidator(realm.PasswordSettings).ValidateAndThrow(payload); | ||
|
||
if (!string.IsNullOrWhiteSpace(payload.EmailAddress)) | ||
{ | ||
return await HandleEmailAddressAsync(payload.EmailAddress, payload.Locale, cancellationToken); | ||
} | ||
else if (payload.Token != null && payload.NewPassword != null) | ||
{ | ||
return await HandleNewPasswordAsync(payload.Token, payload.NewPassword, command.CustomAttributes, cancellationToken); | ||
} | ||
|
||
throw new ArgumentException($"The '{nameof(command.Payload)}' is not valid.", nameof(command)); | ||
} | ||
|
||
private async Task<ResetPasswordResult> HandleEmailAddressAsync(string emailAddress, string locale, CancellationToken cancellationToken) | ||
{ | ||
SentMessages sentMessages; | ||
|
||
User? user = await _userService.FindAsync(emailAddress, cancellationToken); | ||
if (user != null) | ||
{ | ||
CreatedToken token = await _tokenService.CreateAsync(user.GetSubject(), TokenTypes.PasswordRecovery, cancellationToken); | ||
Dictionary<string, string> variables = new() | ||
{ | ||
["Token"] = token.Token | ||
}; | ||
sentMessages = await _messageService.SendAsync(Templates.PasswordRecovery, user, ContactType.Email, locale, variables, cancellationToken); | ||
} | ||
else | ||
{ | ||
sentMessages = new([Guid.NewGuid()]); | ||
} | ||
|
||
SentMessage sentMessage = sentMessages.ToSentMessage(new Email(emailAddress)); | ||
return ResetPasswordResult.RecoveryLinkSent(sentMessage); | ||
} | ||
|
||
private async Task<ResetPasswordResult> HandleNewPasswordAsync(string token, string newPassword, IEnumerable<CustomAttribute> customAttributes, CancellationToken cancellationToken) | ||
{ | ||
ValidatedToken validatedToken = await _tokenService.ValidateAsync(token, TokenTypes.PasswordRecovery, cancellationToken); | ||
if (validatedToken.Subject == null) | ||
{ | ||
throw new InvalidOperationException($"The claim '{validatedToken.Subject}' is required."); | ||
} | ||
Guid userId = Guid.Parse(validatedToken.Subject); | ||
_ = await _userService.FindAsync(userId, cancellationToken) ?? throw new InvalidOperationException($"The user 'Id={userId}' could not be found."); | ||
User user = await _userService.ResetPasswordAsync(userId, newPassword, cancellationToken); | ||
|
||
return await EnsureProfileIsCompleted(user, customAttributes, cancellationToken); | ||
} | ||
|
||
private async Task<ResetPasswordResult> EnsureProfileIsCompleted(User user, IEnumerable<CustomAttribute> customAttributes, CancellationToken cancellationToken) | ||
{ | ||
if (!user.IsProfileCompleted()) | ||
{ | ||
CreatedToken token = await _tokenService.CreateAsync(user.GetSubject(), TokenTypes.Profile, cancellationToken); | ||
return ResetPasswordResult.RequireProfileCompletion(token); | ||
} | ||
|
||
Session session = await _sessionService.CreateAsync(user, customAttributes, cancellationToken); | ||
await _publisher.Publish(new UserSignedInEvent(session), cancellationToken); | ||
return ResetPasswordResult.Succeed(session); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
backend/src/Logitar.Master.Application/Accounts/Validators/ResetPasswordValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using FluentValidation; | ||
using Logitar.Identity.Contracts.Settings; | ||
using Logitar.Identity.Domain.Passwords.Validators; | ||
using Logitar.Identity.Domain.Shared; | ||
using Logitar.Master.Contracts.Accounts; | ||
|
||
namespace Logitar.Master.Application.Accounts.Validators; | ||
|
||
internal class ResetPasswordValidator : AbstractValidator<ResetPasswordPayload> | ||
{ | ||
public ResetPasswordValidator(IPasswordSettings passwordSettings) | ||
{ | ||
RuleFor(x => x.Locale).SetValidator(new LocaleValidator()); | ||
|
||
When(x => !string.IsNullOrWhiteSpace(x.EmailAddress), () => | ||
{ | ||
RuleFor(x => x.EmailAddress).NotEmpty(); | ||
RuleFor(x => x.Token).Empty(); | ||
RuleFor(x => x.NewPassword).Empty(); | ||
}).Otherwise(() => | ||
{ | ||
RuleFor(x => x.EmailAddress).Empty(); | ||
RuleFor(x => x.Token).NotEmpty(); | ||
RuleFor(x => x.NewPassword).NotNull(); | ||
When(x => x.NewPassword != null, () => RuleFor(x => x.NewPassword!).SetValidator(new PasswordValidator(passwordSettings))); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
backend/src/Logitar.Master.Contracts/Accounts/ResetPasswordPayload.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
namespace Logitar.Master.Contracts.Accounts; | ||
|
||
public record ResetPasswordPayload | ||
{ | ||
public string Locale { get; set; } | ||
|
||
public string? EmailAddress { get; set; } | ||
|
||
public string? Token { get; set; } | ||
public string? NewPassword { get; set; } | ||
|
||
public ResetPasswordPayload() : this(string.Empty) | ||
{ | ||
} | ||
|
||
public ResetPasswordPayload(string locale) | ||
{ | ||
Locale = locale; | ||
} | ||
|
||
public ResetPasswordPayload(string locale, string emailAddress) : this(locale) | ||
{ | ||
EmailAddress = emailAddress; | ||
} | ||
|
||
public ResetPasswordPayload(string locale, string token, string newPassword) : this(locale) | ||
{ | ||
Token = token; | ||
NewPassword = newPassword; | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
backend/src/Logitar.Master.Contracts/Accounts/ResetPasswordResult.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using Logitar.Portal.Contracts.Sessions; | ||
using Logitar.Portal.Contracts.Tokens; | ||
|
||
namespace Logitar.Master.Contracts.Accounts; | ||
|
||
public record ResetPasswordResult | ||
{ | ||
public SentMessage? RecoveryLinkSentTo { get; set; } | ||
public string? ProfileCompletionToken { get; set; } | ||
public Session? Session { get; set; } | ||
|
||
public ResetPasswordResult() | ||
{ | ||
} | ||
|
||
public static ResetPasswordResult RecoveryLinkSent(SentMessage sentMessage) => new() | ||
{ | ||
RecoveryLinkSentTo = sentMessage | ||
}; | ||
|
||
public static ResetPasswordResult RequireProfileCompletion(CreatedToken createdToken) => new() | ||
{ | ||
ProfileCompletionToken = createdToken.Token | ||
}; | ||
|
||
public static ResetPasswordResult Succeed(Session session) => new() | ||
{ | ||
Session = session | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
backend/src/Logitar.Master/Models/Account/ResetPasswordResponse.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using Logitar.Master.Contracts.Accounts; | ||
|
||
namespace Logitar.Master.Models.Account; | ||
|
||
public record ResetPasswordResponse | ||
{ | ||
public SentMessage? RecoveryLinkSentTo { get; set; } | ||
public string? ProfileCompletionToken { get; set; } | ||
public CurrentUser? CurrentUser { get; set; } | ||
|
||
public ResetPasswordResponse() | ||
{ | ||
} | ||
|
||
public ResetPasswordResponse(ResetPasswordResult result) | ||
{ | ||
RecoveryLinkSentTo = result.RecoveryLinkSentTo; | ||
ProfileCompletionToken = result.ProfileCompletionToken; | ||
|
||
if (result.Session != null) | ||
{ | ||
CurrentUser = new CurrentUser(result.Session.User); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.