This repository was archived by the owner on Jan 23, 2025. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 0
#270 - Reset passwords. #319
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,58 @@ | ||
| // Copyright (c) Dmytro Kyshchenko. All rights reserved. | ||
| // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. | ||
|
|
||
| using Bogus; | ||
| using Pyro.ApiTests.Clients; | ||
| using Pyro.Contracts.Requests.Identity; | ||
|
|
||
| namespace Pyro.ApiTests.Tests; | ||
|
|
||
| public class ResetPasswordTests | ||
| { | ||
| private Faker faker; | ||
| private IdentityClient client; | ||
| private string login; | ||
| private string email; | ||
|
|
||
| [OneTimeSetUp] | ||
| public async Task SetUp() | ||
| { | ||
| faker = new Faker(); | ||
| client = new IdentityClient(Api.BaseAddress); | ||
| await client.Login(); | ||
|
|
||
| login = faker.Random.Hash(32); | ||
| email = faker.Internet.Email(); | ||
| var createUserRequest = new CreateUserRequest(login, email, ["User"]); | ||
| var user = await client.CreateUser(createUserRequest); | ||
| Assert.That(user, Is.Not.Null); | ||
|
|
||
| var message = Api.Smtp.WaitForMessage(x => x.To == email) ?? | ||
| throw new InvalidOperationException("The message was not found."); | ||
| var token = message.GetToken(); | ||
| var password = faker.Random.Hash(); | ||
| var activateUserRequest = new ActivateUserRequest(token, password); | ||
| await client.ActivateUser(activateUserRequest); | ||
| } | ||
|
|
||
| [OneTimeTearDown] | ||
| public void TearDown() | ||
| { | ||
| client.Dispose(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task Tests() | ||
| { | ||
| await client.ForgotPassword(new ForgotPasswordRequest(login)); | ||
| var message = Api.Smtp.WaitForMessage(x => x.To == email) ?? | ||
| throw new InvalidOperationException("The message was not found."); | ||
| var token = message.GetToken(); | ||
|
|
||
| var password = faker.Random.Hash(); | ||
| await client.ResetPassword(new ResetPasswordRequest(token, password)); | ||
|
|
||
| await client.Logout(); | ||
| await client.Login(login, password); | ||
| } | ||
| } |
6 changes: 6 additions & 0 deletions
6
Pyro.Api/Pyro.Contracts/Requests/Identity/ForgotPasswordRequest.cs
This file contains hidden or 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,6 @@ | ||
| // Copyright (c) Dmytro Kyshchenko. All rights reserved. | ||
| // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. | ||
|
|
||
| namespace Pyro.Contracts.Requests.Identity; | ||
|
|
||
| public record ForgotPasswordRequest(string Login); |
6 changes: 6 additions & 0 deletions
6
Pyro.Api/Pyro.Contracts/Requests/Identity/ResetPasswordRequest.cs
This file contains hidden or 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,6 @@ | ||
| // Copyright (c) Dmytro Kyshchenko. All rights reserved. | ||
| // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. | ||
|
|
||
| namespace Pyro.Contracts.Requests.Identity; | ||
|
|
||
| public record ResetPasswordRequest(string Token, string Password); |
This file contains hidden or 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 hidden or 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,80 @@ | ||
| // Copyright (c) Dmytro Kyshchenko. All rights reserved. | ||
| // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. | ||
|
|
||
| using System.Text.Encodings.Web; | ||
| using FluentValidation; | ||
| using MediatR; | ||
| using Microsoft.Extensions.Options; | ||
| using Pyro.Domain.Shared; | ||
| using Pyro.Domain.Shared.Email; | ||
| using Pyro.Domain.Shared.Exceptions; | ||
|
|
||
| namespace Pyro.Domain.Identity.Commands; | ||
|
|
||
| public record ForgotPassword(string Login) : IRequest; | ||
|
|
||
| public class ForgotPasswordValidator : AbstractValidator<ForgotPassword> | ||
| { | ||
| public ForgotPasswordValidator() | ||
| { | ||
| RuleFor(x => x.Login) | ||
| .NotEmpty() | ||
| .MaximumLength(32) | ||
| .Matches(@"^[a-zA-Z0-9_\-]*$"); | ||
| } | ||
| } | ||
|
|
||
| public class ForgotPasswordHandler : IRequestHandler<ForgotPassword> | ||
| { | ||
| private readonly IUserRepository repository; | ||
| private readonly IPasswordService passwordService; | ||
| private readonly IEmailService emailService; | ||
| private readonly EmailServiceOptions emailServiceOptions; | ||
| private readonly ServiceOptions serviceOptions; | ||
| private readonly UrlEncoder urlEncoder; | ||
|
|
||
| public ForgotPasswordHandler( | ||
| IUserRepository repository, | ||
| IPasswordService passwordService, | ||
| IEmailService emailService, | ||
| IOptions<EmailServiceOptions> emailServiceOptions, | ||
| IOptions<ServiceOptions> serviceOptions, | ||
| UrlEncoder urlEncoder) | ||
| { | ||
| this.repository = repository; | ||
| this.passwordService = passwordService; | ||
| this.emailService = emailService; | ||
| this.emailServiceOptions = emailServiceOptions.Value; | ||
| this.serviceOptions = serviceOptions.Value; | ||
| this.urlEncoder = urlEncoder; | ||
| } | ||
|
|
||
| public async Task Handle(ForgotPassword request, CancellationToken cancellationToken = default) | ||
| { | ||
| var user = await repository.GetUserByLogin(request.Login, cancellationToken) ?? | ||
| throw new NotFoundException($"User (Login: {request.Login}) not found"); | ||
|
|
||
| var oneTimePassword = passwordService.GeneratePasswordResetTokenFor(user); | ||
|
|
||
| var inviteLink = new UriBuilder(serviceOptions.PublicUrl!) | ||
| { | ||
| Path = "/reset-password", | ||
| Query = $"token={urlEncoder.Encode(oneTimePassword.Token)}", | ||
| } | ||
| .Uri | ||
| .ToString(); | ||
| var body = $""" | ||
| Hello {user.DisplayName}! | ||
|
|
||
| You have requested to reset your password. Please use the following link to reset your password: <a href="{inviteLink}">Reset Password</a>. If you did not request to reset your password, please ignore this email. | ||
|
|
||
| Thank you, Pyro. | ||
| """; | ||
| var message = new EmailMessage( | ||
| new EmailAddress("No Reply", $"no-reply@{emailServiceOptions.Domain}"), | ||
| new EmailAddress(user.DisplayName, user.Email), | ||
| "Reset your password", | ||
| body); | ||
| await emailService.SendEmail(message, cancellationToken); | ||
| } | ||
| } | ||
This file contains hidden or 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 hidden or 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,58 @@ | ||
| // Copyright (c) Dmytro Kyshchenko. All rights reserved. | ||
| // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. | ||
|
|
||
| using FluentValidation; | ||
| using MediatR; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Pyro.Domain.Identity.Commands; | ||
|
|
||
| public record ResetPassword(string Token, string Password) : IRequest; | ||
|
|
||
| public class ResetPasswordValidator : AbstractValidator<ResetPassword> | ||
| { | ||
| public ResetPasswordValidator() | ||
| { | ||
| RuleFor(x => x.Token) | ||
| .NotEmpty(); | ||
|
|
||
| RuleFor(x => x.Password) | ||
| .NotEmpty() | ||
| .MinimumLength(8); | ||
| } | ||
| } | ||
|
|
||
| public class ResetPasswordHandler : IRequestHandler<ResetPassword> | ||
| { | ||
| private readonly ILogger<ResetPasswordHandler> logger; | ||
| private readonly IUserRepository repository; | ||
| private readonly TimeProvider timeProvider; | ||
| private readonly IPasswordService passwordService; | ||
|
|
||
| public ResetPasswordHandler( | ||
| ILogger<ResetPasswordHandler> logger, | ||
| IUserRepository repository, | ||
| TimeProvider timeProvider, | ||
| IPasswordService passwordService) | ||
| { | ||
| this.logger = logger; | ||
| this.repository = repository; | ||
| this.timeProvider = timeProvider; | ||
| this.passwordService = passwordService; | ||
| } | ||
|
|
||
| public async Task Handle(ResetPassword request, CancellationToken cancellationToken = default) | ||
| { | ||
| var user = await repository.GetUserByToken(request.Token, cancellationToken); | ||
| var otp = user?.GetOneTimePassword(request.Token); | ||
| if (user is null || otp is null) | ||
| { | ||
| logger.LogError("The token '{Token}' is invalid", request.Token); | ||
| return; | ||
| } | ||
|
|
||
| user.ResetPassword(timeProvider, passwordService, otp, request.Password); | ||
|
|
||
| logger.LogInformation("User '{Login}' password reset", user.Login); | ||
| } | ||
| } |
This file contains hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.