Add user avatar and profile feature, consolidate Auth and UserManagement features (#257)

This commit is contained in:
Aaron Po
2026-08-31 00:11:48 -04:00
committed by GitHub
parent ade2128a73
commit 39c5a89ca7
130 changed files with 1005 additions and 613 deletions

View File

@@ -0,0 +1,15 @@
using Features.Auth.Dtos;
using MediatR;
namespace Features.Auth.Commands.Account.UpdateEmail;
/// <summary>
/// Changes the email address of the given user account. The account's <c>EmailConfirmed</c> status
/// is reset to unconfirmed as a result.
/// </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 UpdateEmailCommand(Guid UserAccountId, string NewEmail)
: IRequest<UpdateEmailPayload>;

View File

@@ -0,0 +1,41 @@
using Domain.Exceptions;
using Features.Auth.Dtos;
using Features.Auth.Identity;
using MediatR;
using Microsoft.AspNetCore.Identity;
namespace Features.Auth.Commands.Account.UpdateEmail;
/// <summary>
/// Handles <see cref="UpdateEmailCommand" /> via <see cref="UserManager{TUser}" />.
/// <see cref="UserManager{TUser}.SetEmailAsync" /> also resets <c>EmailConfirmed</c> to
/// <see langword="false" />, so the user must re-confirm the new address (see
/// <c>Features.Users.Commands.ResendConfirmationEmail</c>).
/// </summary>
public class UpdateEmailHandler(UserManager<ApplicationUser> userManager)
: IRequestHandler<UpdateEmailCommand, UpdateEmailPayload>
{
/// <exception cref="NotFoundException">Thrown when the account does not exist.</exception>
/// <exception cref="ConflictException">Thrown when the requested email address is already in use.</exception>
public async Task<UpdateEmailPayload> Handle(
UpdateEmailCommand request,
CancellationToken cancellationToken
)
{
ApplicationUser user =
await userManager.FindByIdAsync(request.UserAccountId.ToString())
?? throw new NotFoundException("User account not found");
IdentityResult result = await userManager.SetEmailAsync(user, request.NewEmail);
if (!result.Succeeded)
{
if (result.Errors.Any(e => e.Code == "DuplicateEmail"))
throw new ConflictException("Email address already exists");
throw new ConflictException(
string.Join("; ", result.Errors.Select(e => e.Description))
);
}
return new UpdateEmailPayload(user.Id, user.Email, user.EmailConfirmed);
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
namespace Features.Auth.Commands.Account.UpdateEmail;
/// <summary>Validates <see cref="UpdateEmailCommand" />, mirroring the email rules in <c>RegisterUserValidator</c>.</summary>
public class UpdateEmailValidator : AbstractValidator<UpdateEmailCommand>
{
public UpdateEmailValidator()
{
RuleFor(x => x.NewEmail)
.NotEmpty()
.WithMessage("Email is required")
.EmailAddress()
.WithMessage("Invalid email format")
.MaximumLength(128)
.WithMessage("Email cannot exceed 128 characters");
}
}