diff --git a/LeaderboardBackend.Test/Features/Users/AccountConfirmationTests.cs b/LeaderboardBackend.Test/Features/Users/AccountConfirmationTests.cs index e6930d98..0c3187db 100644 --- a/LeaderboardBackend.Test/Features/Users/AccountConfirmationTests.cs +++ b/LeaderboardBackend.Test/Features/Users/AccountConfirmationTests.cs @@ -107,7 +107,7 @@ public async Task ResendConfirmation_Success() emailSenderMock.Verify(x => x.EnqueueEmailAsync( "email1", - AccountController.ACCOUNT_CONFIRMATION_EMAIL_TITLE, + "Confirmation", It.IsAny() ), Times.Once() diff --git a/LeaderboardBackend/Controllers/AccountController.cs b/LeaderboardBackend/Controllers/AccountController.cs index 1bfda655..9648643f 100644 --- a/LeaderboardBackend/Controllers/AccountController.cs +++ b/LeaderboardBackend/Controllers/AccountController.cs @@ -5,6 +5,7 @@ using LeaderboardBackend.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using OneOf; namespace LeaderboardBackend.Controllers; @@ -13,8 +14,6 @@ namespace LeaderboardBackend.Controllers; public class AccountController : ControllerBase { private readonly IUserService _userService; - // TODO: Finalise the title - public const string ACCOUNT_CONFIRMATION_EMAIL_TITLE = "Confirmation"; public AccountController(IUserService userService) { @@ -123,6 +122,7 @@ public async Task> Login([FromBody] LoginRequest req /// Resends the account confirmation link. /// /// IAuthService dependency. + /// IConfirmationService dependency. /// EmailSender dependency. /// The request was sent successfully. /// @@ -131,9 +131,6 @@ public async Task> Login([FromBody] LoginRequest req /// /// The request doesn't contain a valid session token. /// - /// - /// The `User` the request is for wasn't found. - /// /// /// A `User` with the specified username or email already exists.

/// Validation error codes by property: @@ -142,13 +139,16 @@ public async Task> Login([FromBody] LoginRequest req /// - **Email**: /// - **EmailAlreadyUsed**: the email is already in use ///
+ /// + /// Internal server error. + /// [HttpPost("confirm")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status429TooManyRequests)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task ResendConfirmation( [FromServices] IAuthService authService, [FromServices] IConfirmationService confirmationService, @@ -157,40 +157,23 @@ [FromServices] IEmailSender emailSender { // TODO: Handle rate limiting (429 case) - zysim - Guid? userId = authService.GetUserIdFromClaims(HttpContext.User); - - if (userId is null) - { - return Unauthorized(); - } - - User? user = await _userService.GetUserById(userId ?? Guid.Empty); + GetUserResult result = await _userService.GetRegisteredUserFromClaims(HttpContext.User); - if (user is null) + if (result.TryPickT0(out User user, out OneOf errors)) { - return NotFound(); + CreateUserConfirmationResult confirmationResult = await confirmationService.CreateConfirmation(user); + return confirmationResult.Match( + confirmation => Ok(), + dbCreateFailed => StatusCode(StatusCodes.Status500InternalServerError), + dbTimedOut => StatusCode(StatusCodes.Status500InternalServerError) + ); } - if (user.Role is not UserRole.Registered) - { - return Conflict(); - } - - UserConfirmation confirmation = await confirmationService.CreateConfirmation(user); - -#pragma warning disable CS4014 // Suppress no 'await' call - emailSender.EnqueueEmailAsync( - user.Email, - ACCOUNT_CONFIRMATION_EMAIL_TITLE, - // TODO: Generate confirmation link - GenerateAccountConfirmationEmailBody(user, confirmation) + return errors.Match( + badCredentials => Unauthorized(), + // Shouldn't be possible; throw 500 + notFound => StatusCode(StatusCodes.Status500InternalServerError), + badRole => Conflict() ); -#pragma warning restore CS4014 - - return Ok(); } - - // TODO: Finalise message contents - private string GenerateAccountConfirmationEmailBody(User user, UserConfirmation confirmation) => - $@"Hi {user.Username},

Click here to confirm your account."; } diff --git a/LeaderboardBackend/Services/IConfirmationService.cs b/LeaderboardBackend/Services/IConfirmationService.cs index 938a7372..15d7b0c9 100644 --- a/LeaderboardBackend/Services/IConfirmationService.cs +++ b/LeaderboardBackend/Services/IConfirmationService.cs @@ -1,9 +1,16 @@ using LeaderboardBackend.Models.Entities; +using OneOf; namespace LeaderboardBackend.Services; public interface IConfirmationService { Task GetConfirmationById(Guid id); - Task CreateConfirmation(User user); + Task CreateConfirmation(User user, CancellationToken token = default); } + +[GenerateOneOf] +public partial class CreateUserConfirmationResult : OneOfBase { }; + +public readonly record struct DbCreateFailed(); +public readonly record struct DbCreateTimedOut(); diff --git a/LeaderboardBackend/Services/Impl/ConfirmationService.cs b/LeaderboardBackend/Services/Impl/ConfirmationService.cs index 7ad62a17..2f29cf62 100644 --- a/LeaderboardBackend/Services/Impl/ConfirmationService.cs +++ b/LeaderboardBackend/Services/Impl/ConfirmationService.cs @@ -1,14 +1,17 @@ using LeaderboardBackend.Models.Entities; +using Microsoft.EntityFrameworkCore; namespace LeaderboardBackend.Services; public class ConfirmationService : IConfirmationService { private readonly ApplicationContext _applicationContext; + private readonly IEmailSender _emailSender; - public ConfirmationService(ApplicationContext applicationContext) + public ConfirmationService(ApplicationContext applicationContext, IEmailSender emailSender) { _applicationContext = applicationContext; + _emailSender = emailSender; } public async Task GetConfirmationById(Guid id) @@ -16,7 +19,7 @@ public ConfirmationService(ApplicationContext applicationContext) return await _applicationContext.UserConfirmations.FindAsync(id); } - public async Task CreateConfirmation(User user) + public async Task CreateConfirmation(User user, CancellationToken token = default) { UserConfirmation newConfirmation = new() @@ -26,8 +29,33 @@ public async Task CreateConfirmation(User user) _applicationContext.UserConfirmations.Add(newConfirmation); - await _applicationContext.SaveChangesAsync(); + try + { + await _applicationContext.SaveChangesAsync(token); + } + catch (DbUpdateException) + { + return new DbCreateFailed(); + } + catch (OperationCanceledException) + { + return new DbCreateTimedOut(); + } + +#pragma warning disable CS4014 // Suppress no 'await' call + _emailSender.EnqueueEmailAsync( + user.Email, + // TODO: Finalise the title + "Confirmation", + // TODO: Generate confirmation link + GenerateAccountConfirmationEmailBody(user, newConfirmation) + ); +#pragma warning restore CS4014 return newConfirmation; } + + // TODO: Finalise message contents + private string GenerateAccountConfirmationEmailBody(User user, UserConfirmation confirmation) => + $@"Hi {user.Username},

Click here to confirm your account."; }