mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Add self-service account management commands (delete, update username/email/password/profile)
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
using Features.Auth.Dtos;
|
||||
using MediatR;
|
||||
|
||||
namespace Features.Auth.Commands.UpdatePassword;
|
||||
|
||||
/// <summary>Changes the password of the given user account, verifying the current password first.</summary>
|
||||
/// <param name="UserAccountId">
|
||||
/// The authenticated caller's own ID, extracted from the access token -- never bind this from
|
||||
/// client-supplied input.
|
||||
/// </param>
|
||||
public record UpdatePasswordCommand(Guid UserAccountId, string CurrentPassword, string NewPassword)
|
||||
: IRequest<UpdatePasswordPayload>;
|
||||
@@ -0,0 +1,42 @@
|
||||
using Domain.Exceptions;
|
||||
using Features.Auth.Dtos;
|
||||
using Features.Auth.Identity;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Features.Auth.Commands.UpdatePassword;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="UpdatePasswordCommand" /> via <see cref="UserManager{TUser}.ChangePasswordAsync" />,
|
||||
/// which verifies the current password before hashing and persisting the new one.
|
||||
/// </summary>
|
||||
public class UpdatePasswordHandler(UserManager<ApplicationUser> userManager)
|
||||
: IRequestHandler<UpdatePasswordCommand, UpdatePasswordPayload>
|
||||
{
|
||||
/// <exception cref="NotFoundException">Thrown when the account does not exist.</exception>
|
||||
/// <exception cref="UnauthorizedException">Thrown when the current password does not match.</exception>
|
||||
/// <exception cref="ConflictException">Thrown when the update otherwise fails.</exception>
|
||||
public async Task<UpdatePasswordPayload> Handle(
|
||||
UpdatePasswordCommand request,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
ApplicationUser user =
|
||||
await userManager.FindByIdAsync(request.UserAccountId.ToString())
|
||||
?? throw new NotFoundException("User account not found");
|
||||
|
||||
IdentityResult result = await userManager.ChangePasswordAsync(
|
||||
user,
|
||||
request.CurrentPassword,
|
||||
request.NewPassword
|
||||
);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
if (result.Errors.Any(e => e.Code == "PasswordMismatch"))
|
||||
throw new UnauthorizedException("Current password is incorrect");
|
||||
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
|
||||
}
|
||||
|
||||
return new UpdatePasswordPayload(user.Id, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace Features.Auth.Commands.UpdatePassword;
|
||||
|
||||
/// <summary>Validates <see cref="UpdatePasswordCommand" />, mirroring the password rules in <c>RegisterUserValidator</c>.</summary>
|
||||
public class UpdatePasswordValidator : AbstractValidator<UpdatePasswordCommand>
|
||||
{
|
||||
public UpdatePasswordValidator()
|
||||
{
|
||||
RuleFor(x => x.CurrentPassword).NotEmpty().WithMessage("Current password is required");
|
||||
|
||||
RuleFor(x => x.NewPassword)
|
||||
.NotEmpty()
|
||||
.WithMessage("New password is required")
|
||||
.MinimumLength(8)
|
||||
.WithMessage("Password must be at least 8 characters")
|
||||
.Matches("[A-Z]")
|
||||
.WithMessage("Password must contain at least one uppercase letter")
|
||||
.Matches("[a-z]")
|
||||
.WithMessage("Password must contain at least one lowercase letter")
|
||||
.Matches("[0-9]")
|
||||
.WithMessage("Password must contain at least one number")
|
||||
.Matches("[^a-zA-Z0-9]")
|
||||
.WithMessage("Password must contain at least one special character")
|
||||
.NotEqual(x => x.CurrentPassword)
|
||||
.WithMessage("New password must be different from the current password");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user