Format ./web/backend/Features/Features.Auth

This commit is contained in:
Aaron Po
2026-06-20 15:09:40 -04:00
parent 5b882ac51c
commit 2bdd7bb0c0
21 changed files with 189 additions and 146 deletions

View File

@@ -7,4 +7,4 @@ namespace Features.Auth.Commands.ConfirmUser;
/// Validates a confirmation token and confirms the corresponding user account.
/// </summary>
/// <param name="Token">The confirmation token issued to the user, typically delivered via email.</param>
public record ConfirmUserCommand(string Token) : IRequest<ConfirmationPayload>;
public record ConfirmUserCommand(string Token) : IRequest<ConfirmationPayload>;

View File

@@ -13,22 +13,26 @@ namespace Features.Auth.Commands.ConfirmUser;
/// </summary>
/// <param name="authRepository">Repository used to look up and confirm user accounts.</param>
/// <param name="tokenService">Service used to validate the confirmation token.</param>
public class ConfirmUserHandler(
IAuthRepository authRepository,
ITokenService tokenService
) : IRequestHandler<ConfirmUserCommand, ConfirmationPayload>
public class ConfirmUserHandler(IAuthRepository authRepository, ITokenService tokenService)
: IRequestHandler<ConfirmUserCommand, ConfirmationPayload>
{
/// <exception cref="UnauthorizedException">
/// Thrown when the confirmation token is invalid or expired, or when the associated user account cannot be found.
/// </exception>
public async Task<ConfirmationPayload> Handle(ConfirmUserCommand request, CancellationToken cancellationToken)
public async Task<ConfirmationPayload> Handle(
ConfirmUserCommand request,
CancellationToken cancellationToken
)
{
ValidatedToken validatedToken = await tokenService.ValidateConfirmationTokenAsync(request.Token);
ValidatedToken validatedToken = await tokenService.ValidateConfirmationTokenAsync(
request.Token
);
UserAccount? user = await authRepository.ConfirmUserAccountAsync(validatedToken.UserId);
if (user == null) throw new UnauthorizedException("User account not found");
if (user == null)
throw new UnauthorizedException("User account not found");
return new ConfirmationPayload(user.UserAccountId, DateTime.UtcNow);
}
}
}

View File

@@ -7,4 +7,4 @@ namespace Features.Auth.Commands.RefreshToken;
/// Exchanges a valid refresh token for a new access/refresh token pair. Bound directly from the
/// request body of <c>POST /api/auth/refresh</c>.
/// </summary>
public record RefreshTokenCommand(string RefreshToken) : IRequest<LoginPayload>;
public record RefreshTokenCommand(string RefreshToken) : IRequest<LoginPayload>;

View File

@@ -12,10 +12,17 @@ namespace Features.Auth.Commands.RefreshToken;
public class RefreshTokenHandler(ITokenService tokenService)
: IRequestHandler<RefreshTokenCommand, LoginPayload>
{
public async Task<LoginPayload> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
public async Task<LoginPayload> Handle(
RefreshTokenCommand request,
CancellationToken cancellationToken
)
{
RefreshTokenResult result = await tokenService.RefreshTokenAsync(request.RefreshToken);
return new LoginPayload(result.UserAccount.UserAccountId, result.UserAccount.Username, result.RefreshToken,
result.AccessToken);
return new LoginPayload(
result.UserAccount.UserAccountId,
result.UserAccount.Username,
result.RefreshToken,
result.AccessToken
);
}
}
}

View File

@@ -12,8 +12,6 @@ public class RefreshTokenValidator : AbstractValidator<RefreshTokenCommand>
/// </summary>
public RefreshTokenValidator()
{
RuleFor(x => x.RefreshToken)
.NotEmpty()
.WithMessage("Refresh token is required");
RuleFor(x => x.RefreshToken).NotEmpty().WithMessage("Refresh token is required");
}
}
}

View File

@@ -25,4 +25,4 @@ public record RegisterUserCommand(
string Email,
DateTime DateOfBirth,
string Password
) : IRequest<RegistrationPayload>;
) : IRequest<RegistrationPayload>;

View File

@@ -28,7 +28,10 @@ public class RegisterUserHandler(
/// <exception cref="ConflictException">
/// Thrown when an existing account already has the same username or email address.
/// </exception>
public async Task<RegistrationPayload> Handle(RegisterUserCommand request, CancellationToken cancellationToken)
public async Task<RegistrationPayload> Handle(
RegisterUserCommand request,
CancellationToken cancellationToken
)
{
await ValidateUserDoesNotExist(request.Username, request.Email);
@@ -48,14 +51,23 @@ public class RegisterUserHandler(
string confirmationToken = tokenService.GenerateConfirmationToken(createdUser);
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return new RegistrationPayload(createdUser.UserAccountId, createdUser.Username, string.Empty, string.Empty,
false);
return new RegistrationPayload(
createdUser.UserAccountId,
createdUser.Username,
string.Empty,
string.Empty,
false
);
bool emailSent = false;
try
{
await mediator.Send(
new SendRegistrationEmailCommand(createdUser.FirstName, createdUser.Email, confirmationToken),
new SendRegistrationEmailCommand(
createdUser.FirstName,
createdUser.Email,
confirmationToken
),
cancellationToken
);
emailSent = true;
@@ -67,8 +79,13 @@ public class RegisterUserHandler(
// ignored
}
return new RegistrationPayload(createdUser.UserAccountId, createdUser.Username, refreshToken, accessToken,
emailSent);
return new RegistrationPayload(
createdUser.UserAccountId,
createdUser.Username,
refreshToken,
accessToken,
emailSent
);
}
/// <exception cref="ConflictException">
@@ -82,4 +99,4 @@ public class RegisterUserHandler(
if (existingUsername != null || existingEmail != null)
throw new ConflictException("Username or email already exists");
}
}
}

View File

@@ -61,8 +61,6 @@ public class RegisterUserValidator : AbstractValidator<RegisterUserCommand>
.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"
);
.WithMessage("Password must contain at least one special character");
}
}
}

View File

@@ -6,4 +6,4 @@ namespace Features.Auth.Commands.ResendConfirmationEmail;
/// Resends the account confirmation email to a user, generating a fresh confirmation token.
/// </summary>
/// <param name="UserId">The unique identifier of the user requesting the resend.</param>
public record ResendConfirmationEmailCommand(Guid UserId) : IRequest;
public record ResendConfirmationEmailCommand(Guid UserId) : IRequest;

View File

@@ -23,12 +23,17 @@ public class ResendConfirmationEmailHandler(
IMediator mediator
) : IRequestHandler<ResendConfirmationEmailCommand>
{
public async Task Handle(ResendConfirmationEmailCommand request, CancellationToken cancellationToken)
public async Task Handle(
ResendConfirmationEmailCommand request,
CancellationToken cancellationToken
)
{
UserAccount? user = await authRepository.GetUserByIdAsync(request.UserId);
if (user == null) return; // Silent return to prevent user enumeration
if (user == null)
return; // Silent return to prevent user enumeration
if (await authRepository.IsUserVerifiedAsync(request.UserId)) return; // Already confirmed, no-op
if (await authRepository.IsUserVerifiedAsync(request.UserId))
return; // Already confirmed, no-op
string confirmationToken = tokenService.GenerateConfirmationToken(user);
await mediator.Send(
@@ -36,4 +41,4 @@ public class ResendConfirmationEmailHandler(
cancellationToken
);
}
}
}