From 693f74bb5b79b8d81fafe1a80903e3d7077aa802 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Tue, 18 Aug 2026 16:28:25 -0400 Subject: [PATCH] Update XML doc comment in backend projects --- .../Controllers/NotFoundController.cs | 3 ++ web/backend/API/API.Core/Program.cs | 7 ++-- .../RefreshToken/RefreshTokenHandler.cs | 4 +++ .../RefreshToken/RefreshTokenValidator.cs | 1 + .../RegisterUser/RegisterUserValidator.cs | 4 +++ .../Controllers/AuthController.cs | 25 +++++++++++++ ...FeaturesAuthServiceCollectionExtensions.cs | 5 +++ .../Features.Auth/Dtos/UserRegistrationDto.cs | 2 ++ .../Queries/Login/LoginValidator.cs | 1 + .../Repository/AuthRepository.cs | 33 ++++++++--------- .../Repository/IAuthRepository.cs | 11 ++++++ .../Features.Auth/Services/ITokenService.cs | 29 +++++++++++++++ .../Features.Auth/Services/TokenService.cs | 19 ++++------ .../CreateBrewery/CreateBreweryCommand.cs | 2 ++ .../CreateBrewery/CreateBreweryHandler.cs | 4 +++ .../CreateBrewery/CreateBreweryValidator.cs | 1 + .../DeleteBrewery/DeleteBreweryCommand.cs | 1 + .../DeleteBrewery/DeleteBreweryHandler.cs | 4 +++ .../UpdateBrewery/UpdateBreweryCommand.cs | 5 +++ .../UpdateBrewery/UpdateBreweryHandler.cs | 1 + .../UpdateBrewery/UpdateBreweryValidator.cs | 1 + .../Controllers/BreweryController.cs | 10 +++++- ...resBreweriesServiceCollectionExtensions.cs | 3 ++ .../Features.Breweries/Dtos/BreweryDto.cs | 4 +++ .../Dtos/BreweryDtoMapper.cs | 5 +++ .../GetAllBreweries/GetAllBreweriesHandler.cs | 2 ++ .../GetAllBreweries/GetAllBreweriesQuery.cs | 3 ++ .../GetBreweryById/GetBreweryByIdHandler.cs | 2 ++ .../GetBreweryById/GetBreweryByIdQuery.cs | 2 ++ .../Repository/BreweryRepository.cs | 35 +++++-------------- .../Repository/IBreweryRepository.cs | 22 ++++++++++-- .../Features.Emails.Tests.csproj | 2 +- ...aturesEmailsServiceCollectionExtensions.cs | 7 ++++ .../Services/EmailDispatcher.cs | 2 ++ .../Services/IEmailDispatcher.cs | 4 +++ ...resLocationsServiceCollectionExtensions.cs | 7 ++++ .../Commands/UpdateUser/UpdateUserHandler.cs | 3 ++ .../Controllers/UserController.cs | 9 +++++ ...erManagementServiceCollectionExtensions.cs | 7 ++++ .../Queries/GetAllUsers/GetAllUsersHandler.cs | 3 ++ .../Queries/GetAllUsers/GetAllUsersQuery.cs | 5 +++ .../Queries/GetUserById/GetUserByIdHandler.cs | 3 ++ .../Queries/GetUserById/GetUserByIdQuery.cs | 3 ++ .../Repository/IUserAccountRepository.cs | 6 ++++ .../Repository/UserAccountRepository.cs | 2 -- .../Rendering/EmailTemplateProvider.cs | 2 ++ .../Rendering/IEmailTemplateProvider.cs | 14 ++++---- .../Infrastructure.Email/IEmailProvider.cs | 10 ++++++ .../Infrastructure.Email/SmtpEmailProvider.cs | 6 ++-- ...structureSqlServiceCollectionExtensions.cs | 3 ++ .../Infrastructure.Sql/SqlErrorCodes.cs | 17 +++++---- .../Behaviors/ValidationBehavior.cs | 1 + .../Shared/Shared.Contracts/ResponseBody.cs | 3 ++ 53 files changed, 287 insertions(+), 83 deletions(-) diff --git a/web/backend/API/API.Core/Controllers/NotFoundController.cs b/web/backend/API/API.Core/Controllers/NotFoundController.cs index 58a1a8af..2f0ee490 100644 --- a/web/backend/API/API.Core/Controllers/NotFoundController.cs +++ b/web/backend/API/API.Core/Controllers/NotFoundController.cs @@ -14,6 +14,9 @@ namespace API.Core.Controllers; [Route("error")] // required public class NotFoundController : ControllerBase { + /// + /// Returns a 404 response for any request that did not match a route. + /// [HttpGet("404")] //required public IActionResult Handle404() { diff --git a/web/backend/API/API.Core/Program.cs b/web/backend/API/API.Core/Program.cs index 474e7176..5e9b85d2 100644 --- a/web/backend/API/API.Core/Program.cs +++ b/web/backend/API/API.Core/Program.cs @@ -16,7 +16,7 @@ using Shared.Application.Behaviors; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -// Global Exception Filter +// Add current controllers to the exception filter builder .Services.AddControllers(options => { @@ -37,7 +37,7 @@ builder.Services.AddValidatorsFromAssembly(typeof(UserController).Assembly); builder.Services.AddValidatorsFromAssembly(typeof(AuthController).Assembly); builder.Services.AddFluentValidationAutoValidation(); -// Add MediatR. Each Features.* slice's assembly is registered here as it's introduced; +// Add MediatR. // ValidationBehavior runs FluentValidation validators in the MediatR pipeline for command/query handlers. builder.Services.AddMediatR(cfg => { @@ -67,8 +67,7 @@ builder.Services.AddFeaturesUserManagement(); builder.Services.AddFeaturesAuth(); builder.Services.AddFeaturesEmails(); -// ITokenInfrastructure is registered here (not inside Features.Auth's own DI extension) because -// JwtAuthenticationHandler, a host-level auth middleware concern, also depends on it directly. +// ITokenInfrastructure is registered here because JwtAuthenticationHandler depends on it directly. builder.Services.AddScoped(); // Register the exception filter diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs index 4b311b73..6f0a520b 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs @@ -11,6 +11,10 @@ namespace Features.Auth.Commands.RefreshToken; public class RefreshTokenHandler(ITokenService tokenService) : IRequestHandler { + /// + /// Thrown when the refresh token is invalid or expired, or when the user account it refers to no + /// longer exists. + /// public async Task Handle( RefreshTokenCommand request, CancellationToken cancellationToken diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs index 19489ddf..d25bcf68 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs @@ -2,6 +2,7 @@ using FluentValidation; namespace Features.Auth.Commands.RefreshToken; +/// Validates that is present. public class RefreshTokenValidator : AbstractValidator { public RefreshTokenValidator() diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs index de31bddf..10e1075a 100644 --- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs +++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs @@ -2,6 +2,10 @@ using FluentValidation; namespace Features.Auth.Commands.RegisterUser; +/// +/// Validates the field-level constraints on : username format and +/// length, name lengths, email format, minimum age of 19, and password complexity. +/// public class RegisterUserValidator : AbstractValidator { public RegisterUserValidator() diff --git a/web/backend/Features/Features.Auth/Controllers/AuthController.cs b/web/backend/Features/Features.Auth/Controllers/AuthController.cs index 2e0aa868..0369ba9c 100644 --- a/web/backend/Features/Features.Auth/Controllers/AuthController.cs +++ b/web/backend/Features/Features.Auth/Controllers/AuthController.cs @@ -23,6 +23,11 @@ namespace Features.Auth.Controllers; [Authorize(AuthenticationSchemes = "JWT")] public class AuthController(IMediator mediator) : ControllerBase { + /// Registers a new user account. + /// + /// Anonymous access. Returns 201 Created with the new account's ID, username, and issued tokens. + /// Returns 409 Conflict if the username or email is already taken. + /// [AllowAnonymous] [HttpPost("register")] public async Task>> Register( @@ -40,6 +45,10 @@ public class AuthController(IMediator mediator) : ControllerBase ); } + /// Authenticates a user by username and password and issues a new access/refresh token pair. + /// + /// Anonymous access. Returns 200 OK with the tokens, or 401 Unauthorized if the credentials are invalid. + /// [AllowAnonymous] [HttpPost("login")] public async Task>> Login([FromBody] LoginQuery query) @@ -54,6 +63,11 @@ public class AuthController(IMediator mediator) : ControllerBase ); } + /// Confirms a user account using a confirmation token. + /// + /// Requires a valid JWT bearer token (no [AllowAnonymous] override). Returns 200 OK on success, + /// or 401 Unauthorized if is invalid or expired. + /// [HttpPost("confirm")] public async Task>> Confirm( [FromQuery] string token @@ -69,6 +83,12 @@ public class AuthController(IMediator mediator) : ControllerBase ); } + /// Resends the account confirmation email for the given user, generating a fresh confirmation token. + /// + /// Requires a valid JWT bearer token (no [AllowAnonymous] override). Always returns 200 OK, even + /// if does not exist or the account is already confirmed, to avoid user + /// enumeration. + /// [HttpPost("confirm/resend")] public async Task> ResendConfirmation([FromQuery] Guid userId) { @@ -76,6 +96,11 @@ public class AuthController(IMediator mediator) : ControllerBase return Ok(new ResponseBody { Message = "confirmation email has been resent" }); } + /// Exchanges a valid refresh token for a new access/refresh token pair. + /// + /// Anonymous access. Returns 200 OK with the new tokens, or 401 Unauthorized if the refresh token is + /// invalid or expired. + /// [AllowAnonymous] [HttpPost("refresh")] public async Task>> Refresh( diff --git a/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs b/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs index c3181b51..d4a27b7d 100644 --- a/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs @@ -5,8 +5,13 @@ using Microsoft.Extensions.DependencyInjection; namespace Features.Auth.DependencyInjection; +/// Registers the services required by Features.Auth with the dependency injection container. public static class FeaturesAuthServiceCollectionExtensions { + /// + /// Adds , , and the Argon2 password hashing + /// infrastructure as scoped services. + /// public static IServiceCollection AddFeaturesAuth(this IServiceCollection services) { services.AddScoped(); diff --git a/web/backend/Features/Features.Auth/Dtos/UserRegistrationDto.cs b/web/backend/Features/Features.Auth/Dtos/UserRegistrationDto.cs index 5a637567..dc436dcf 100644 --- a/web/backend/Features/Features.Auth/Dtos/UserRegistrationDto.cs +++ b/web/backend/Features/Features.Auth/Dtos/UserRegistrationDto.cs @@ -1,5 +1,7 @@ namespace Features.Auth.Dtos; +/// Data required to persist a new user account and its initial credential. +/// The already-hashed password; never a plaintext value. public record UserRegistrationDto( string Username, string FirstName, diff --git a/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs b/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs index c90e9fb7..cb0f540c 100644 --- a/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs +++ b/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs @@ -2,6 +2,7 @@ using FluentValidation; namespace Features.Auth.Queries.Login; +/// Validates that and are present. public class LoginValidator : AbstractValidator { public LoginValidator() diff --git a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs index fc200c44..2b145b3d 100644 --- a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs +++ b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs @@ -19,11 +19,8 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) private const string SelectColumns = "UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, Timer"; - /// - /// Registers a new user account and its initial credential in a single transaction, then fetches - /// and returns the newly created user. - /// - /// Thrown when the newly registered user cannot be retrieved after registration. + /// + /// The account and credential are inserted within a single database transaction. public async Task RegisterUserAsync(UserRegistrationDto userRegistrationDto) { await using DbConnection connection = await CreateConnection(); @@ -69,7 +66,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) ?? throw new Exception("Failed to retrieve newly registered user."); } - /// Retrieves a user account by email. + /// public async Task GetUserByEmailAsync(string email) { await using DbConnection connection = await CreateConnection(); @@ -79,7 +76,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) ); } - /// Retrieves a user account by username. + /// public async Task GetUserByUsernameAsync(string username) { await using DbConnection connection = await CreateConnection(); @@ -89,7 +86,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) ); } - /// Retrieves the active (non-revoked) credential for a user account. + /// public async Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId) { await using DbConnection connection = await CreateConnection(); @@ -103,10 +100,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) ); } - /// - /// Rotates a user's credential by revoking all existing credentials and creating a new one. - /// - /// Thrown when no account with exists. + /// public async Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash) { await using DbConnection connection = await CreateConnection(); @@ -151,7 +145,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) } } - /// Retrieves a user account by ID. + /// public async Task GetUserByIdAsync(Guid userAccountId) { await using DbConnection connection = await CreateConnection(); @@ -161,11 +155,12 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) ); } - /// - /// Marks a user account as confirmed by inserting a verification record. If the user is already - /// verified, this is a no-op and the existing user is returned (idempotent). If a concurrent request - /// verifies the user first, the resulting duplicate-key SQL exception (error 2601/2627) is swallowed. - /// + /// + /// + /// Confirmation is recorded by inserting a row into dbo.UserVerification. If a concurrent + /// request verifies the same user first, the resulting duplicate-key SQL exception (error 2601/2627) + /// is swallowed to keep the operation idempotent. + /// /// /// Thrown when the database command fails for a reason other than a duplicate verification record. /// @@ -197,7 +192,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) return await GetUserByIdAsync(userAccountId); } - /// Checks whether a user account has been verified by querying the dbo.UserVerification table. + /// public async Task IsUserVerifiedAsync(Guid userAccountId) { await using DbConnection connection = await CreateConnection(); diff --git a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs index c5ac13a0..8a61cbb0 100644 --- a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs +++ b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs @@ -8,21 +8,32 @@ namespace Features.Auth.Repository; /// public interface IAuthRepository { + /// + /// Registers a new user account and its initial credential as a single atomic operation, then + /// returns the created account. + /// + /// Thrown when the newly registered user cannot be retrieved after registration. Task RegisterUserAsync(UserRegistrationDto userRegistrationDto); + /// Retrieves a user account by email, or if none exists. Task GetUserByEmailAsync(string email); + /// Retrieves a user account by username, or if none exists. Task GetUserByUsernameAsync(string username); + /// Retrieves the active (non-revoked) credential for a user account, or if none exists. Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId); /// Rotates a user's credential by invalidating all existing credentials and creating a new one. + /// Thrown when no account with exists. Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash); /// Marks a user account as confirmed. Idempotent if the account is already verified. Task ConfirmUserAccountAsync(Guid userAccountId); + /// Retrieves a user account by ID, or if none exists. Task GetUserByIdAsync(Guid userAccountId); + /// Determines whether a user account has been verified. Task IsUserVerifiedAsync(Guid userAccountId); } diff --git a/web/backend/Features/Features.Auth/Services/ITokenService.cs b/web/backend/Features/Features.Auth/Services/ITokenService.cs index 5ac81ffc..d953d475 100644 --- a/web/backend/Features/Features.Auth/Services/ITokenService.cs +++ b/web/backend/Features/Features.Auth/Services/ITokenService.cs @@ -29,6 +29,7 @@ public record RefreshTokenResult(UserAccount UserAccount, string RefreshToken, s /// public static class TokenServiceExpirationHours { + /// 1 hour. public const double AccessTokenHours = 1; /// 21 days. @@ -43,10 +44,22 @@ public static class TokenServiceExpirationHours /// public interface ITokenService { + /// + /// Generates a signed access token for the given user, expiring after + /// hours. + /// string GenerateAccessToken(UserAccount user); + /// + /// Generates a signed refresh token for the given user, expiring after + /// hours. + /// string GenerateRefreshToken(UserAccount user); + /// + /// Generates a signed confirmation token for the given user, expiring after + /// hours. + /// string GenerateConfirmationToken(UserAccount user); /// Generates a token of the type specified by . @@ -54,12 +67,28 @@ public interface ITokenService string GenerateToken(UserAccount user) where T : struct, Enum; + /// Validates an access token's signature and expiration and extracts the caller's identity. + /// + /// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation. + /// Task ValidateAccessTokenAsync(string token); + /// Validates a refresh token's signature and expiration and extracts the caller's identity. + /// + /// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation. + /// Task ValidateRefreshTokenAsync(string token); + /// Validates a confirmation token's signature and expiration and extracts the caller's identity. + /// + /// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation. + /// Task ValidateConfirmationTokenAsync(string token); /// Validates a refresh token and issues a new access/refresh token pair for the associated user. + /// + /// Thrown when the refresh token is invalid or expired, or when the user account it refers to no + /// longer exists. + /// Task RefreshTokenAsync(string refreshTokenString); } diff --git a/web/backend/Features/Features.Auth/Services/TokenService.cs b/web/backend/Features/Features.Auth/Services/TokenService.cs index af4bdf3a..c3aa40a7 100644 --- a/web/backend/Features/Features.Auth/Services/TokenService.cs +++ b/web/backend/Features/Features.Auth/Services/TokenService.cs @@ -48,6 +48,7 @@ public class TokenService : ITokenService ); } + /// public string GenerateAccessToken(UserAccount user) { DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours); @@ -59,6 +60,7 @@ public class TokenService : ITokenService ); } + /// public string GenerateRefreshToken(UserAccount user) { DateTime expiresAt = DateTime.UtcNow.AddHours( @@ -72,6 +74,7 @@ public class TokenService : ITokenService ); } + /// public string GenerateConfirmationToken(UserAccount user) { DateTime expiresAt = DateTime.UtcNow.AddHours( @@ -109,33 +112,25 @@ public class TokenService : ITokenService }; } - /// - /// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation. - /// + /// public async Task ValidateAccessTokenAsync(string token) { return await ValidateTokenInternalAsync(token, _accessTokenSecret, "access"); } - /// - /// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation. - /// + /// public async Task ValidateRefreshTokenAsync(string token) { return await ValidateTokenInternalAsync(token, _refreshTokenSecret, "refresh"); } - /// - /// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation. - /// + /// public async Task ValidateConfirmationTokenAsync(string token) { return await ValidateTokenInternalAsync(token, _confirmationTokenSecret, "confirmation"); } - /// - /// Thrown when the refresh token is invalid, or when the user account it refers to no longer exists. - /// + /// public async Task RefreshTokenAsync(string refreshTokenString) { ValidatedToken validated = await ValidateRefreshTokenAsync(refreshTokenString); diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs index 19ad1406..6522accc 100644 --- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs @@ -3,6 +3,8 @@ using MediatR; namespace Features.Breweries.Commands.CreateBrewery; +/// Location details for a brewery being created. +/// Raw binary representation of the brewery's geographic coordinates. public record CreateBreweryLocation( Guid CityId, string AddressLine1, diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs index 324eca28..83b3f285 100644 --- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs @@ -5,10 +5,14 @@ using MediatR; namespace Features.Breweries.Commands.CreateBrewery; +/// Handles by persisting a new brewery post. public class CreateBreweryHandler(IBreweryRepository repository) : IRequestHandler { /// Creates a new brewery post, generating new identifiers for the post and its location. + /// + /// Thrown when 's PostedById or Location.CityId does not exist. + /// public async Task Handle( CreateBreweryCommand request, CancellationToken cancellationToken diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs index 3f9e44f4..481e9393 100644 --- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs @@ -2,6 +2,7 @@ using FluentValidation; namespace Features.Breweries.Commands.CreateBrewery; +/// Validates . public class CreateBreweryValidator : AbstractValidator { public CreateBreweryValidator() diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs index 5e939d16..97fe86f5 100644 --- a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs +++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs @@ -2,4 +2,5 @@ using MediatR; namespace Features.Breweries.Commands.DeleteBrewery; +/// Deletes a brewery post. Bound directly from the route of DELETE /api/brewery/{id}. public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs index 7cf2dac9..e10e9185 100644 --- a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs +++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs @@ -3,9 +3,13 @@ using MediatR; namespace Features.Breweries.Commands.DeleteBrewery; +/// Handles by deleting the corresponding brewery post. public class DeleteBreweryHandler(IBreweryRepository repository) : IRequestHandler { + /// + /// Thrown when no brewery exists with 's BreweryPostId. + /// public Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken) { return repository.DeleteAsync(request.BreweryPostId); diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs index 7a40cdd6..3e33a8c5 100644 --- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs +++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs @@ -3,6 +3,11 @@ using MediatR; namespace Features.Breweries.Commands.UpdateBrewery; +/// Location details for a brewery being updated. +/// +/// Identifier of the existing location row, if any; a new row is created if none exists yet. +/// +/// Raw binary representation of the brewery's geographic coordinates. public record UpdateBreweryLocation( Guid BreweryPostLocationId, Guid CityId, diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs index 05edbee7..821babec 100644 --- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs +++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs @@ -5,6 +5,7 @@ using MediatR; namespace Features.Breweries.Commands.UpdateBrewery; +/// Handles by persisting changes to an existing brewery post. public class UpdateBreweryHandler(IBreweryRepository repository) : IRequestHandler { diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs index 954d13fa..f04e349e 100644 --- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs +++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs @@ -2,6 +2,7 @@ using FluentValidation; namespace Features.Breweries.Commands.UpdateBrewery; +/// Validates . public class UpdateBreweryValidator : AbstractValidator { public UpdateBreweryValidator() diff --git a/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs index 18bc457a..73b8f151 100644 --- a/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs +++ b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs @@ -11,6 +11,7 @@ using Shared.Contracts; namespace Features.Breweries.Controllers; +/// Exposes CRUD operations for brewery posts. /// /// Requires JWT authentication by default; and opt out via /// [AllowAnonymous]. @@ -20,6 +21,7 @@ namespace Features.Breweries.Controllers; [Authorize(AuthenticationSchemes = "JWT")] public class BreweryController(IMediator mediator) : ControllerBase { + /// Retrieves a single brewery post by ID. /// Allows anonymous access. /// 200 OK with the brewery if found; otherwise 404 Not Found. [AllowAnonymous] @@ -39,6 +41,9 @@ public class BreweryController(IMediator mediator) : ControllerBase ); } + /// Retrieves brewery posts, newest first. + /// Maximum number of breweries to return. Unbounded if . + /// Number of breweries to skip. Treated as zero if . /// Allows anonymous access. [AllowAnonymous] [HttpGet] @@ -59,6 +64,7 @@ public class BreweryController(IMediator mediator) : ControllerBase ); } + /// Creates a new brewery post. /// 201 Created with the newly created brewery. [HttpPost] public async Task>> Create( @@ -76,6 +82,7 @@ public class BreweryController(IMediator mediator) : ControllerBase ); } + /// Updates an existing brewery post. /// Must match 's BreweryPostId. /// /// 200 OK with the updated brewery; 400 Bad Request if the route ID does not match the @@ -101,7 +108,8 @@ public class BreweryController(IMediator mediator) : ControllerBase ); } - /// 200 OK confirming the deletion. + /// Deletes a brewery post. + /// 200 OK confirming the deletion; 404 Not Found if no brewery exists with . [HttpDelete("{id:guid}")] public async Task> Delete(Guid id) { diff --git a/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs index 9dacdf19..79770f37 100644 --- a/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs @@ -3,8 +3,11 @@ using Microsoft.Extensions.DependencyInjection; namespace Features.Breweries.DependencyInjection; +/// Registers the services required by the Breweries feature. public static class FeaturesBreweriesServiceCollectionExtensions { + /// Registers and its implementation. + /// , for chaining. public static IServiceCollection AddFeaturesBreweries(this IServiceCollection services) { services.AddScoped(); diff --git a/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs index cbbfc2a3..97e15e6a 100644 --- a/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs +++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs @@ -1,5 +1,6 @@ namespace Features.Breweries.Dtos; +/// Location details of a brewery, as returned by the API. public class BreweryLocationDto { public Guid BreweryPostLocationId { get; set; } @@ -18,6 +19,7 @@ public class BreweryLocationDto public byte[]? Coordinates { get; set; } } +/// A brewery post, as returned by the API. public class BreweryDto { public Guid BreweryPostId { get; set; } @@ -30,10 +32,12 @@ public class BreweryDto public DateTime CreatedAt { get; set; } + /// Gets or sets the date and time of the last edit, or if never edited. public DateTime? UpdatedAt { get; set; } /// Gets or sets the row-version/concurrency token used to detect conflicting concurrent updates. public byte[]? Timer { get; set; } + /// Gets or sets the brewery's location, or if none has been set. public BreweryLocationDto? Location { get; set; } } diff --git a/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs index 89ce7e54..7ba60bb8 100644 --- a/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs +++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs @@ -2,8 +2,13 @@ using Domain.Entities; namespace Features.Breweries.Dtos; +/// Maps domain entities to . public static class BreweryDtoMapper { + /// + /// Converts a to its DTO representation, including its + /// if the entity has one. + /// public static BreweryDto ToDto(this BreweryPost brewery) { return new BreweryDto diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs index 6f06ada8..2b0308c7 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs @@ -5,9 +5,11 @@ using MediatR; namespace Features.Breweries.Queries.GetAllBreweries; +/// Handles by retrieving brewery posts, newest first. public class GetAllBreweriesHandler(IBreweryRepository repository) : IRequestHandler> { + /// Retrieves brewery posts, newest first, applying 's paging. public async Task> Handle( GetAllBreweriesQuery request, CancellationToken cancellationToken diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs index bb572818..96267d81 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs @@ -3,4 +3,7 @@ using MediatR; namespace Features.Breweries.Queries.GetAllBreweries; +/// Retrieves brewery posts, newest first. Bound directly from the query string of GET /api/brewery. +/// Maximum number of breweries to return. Unbounded if . +/// Number of breweries to skip. Treated as zero if . public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest>; diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs index e80ec1c8..0dcef849 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs @@ -5,9 +5,11 @@ using MediatR; namespace Features.Breweries.Queries.GetBreweryById; +/// Handles by retrieving the matching brewery post. public class GetBreweryByIdHandler(IBreweryRepository repository) : IRequestHandler { + /// Retrieves the brewery post, or if none exists with the given ID. public async Task Handle( GetBreweryByIdQuery request, CancellationToken cancellationToken diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs index ece9133d..775edc92 100644 --- a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs +++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs @@ -3,4 +3,6 @@ using MediatR; namespace Features.Breweries.Queries.GetBreweryById; +/// Retrieves a brewery post by ID. Bound directly from the route of GET /api/brewery/{id}. +/// Yields if no brewery exists with the given BreweryPostId. public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs index 06550eb2..0423b0ca 100644 --- a/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs +++ b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs @@ -26,10 +26,11 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) "bp.BreweryPostID, bp.PostedByID, bp.BreweryName, bp.Description, bp.CreatedAt, bp.UpdatedAt, bp.Timer, " + "bpl.BreweryPostLocationID, bpl.CityID, bpl.AddressLine1, bpl.AddressLine2, bpl.PostalCode, bpl.Coordinates"; - /// - /// Retrieves a brewery post by ID, joined to its location. A brewery with no location row - /// returns null (matches the previous stored procedure's INNER JOIN behavior). - /// + /// + /// + /// Joins to the location table with an INNER JOIN (matching the previous stored procedure's + /// behavior), which is why a brewery with no location row is treated as not found. + /// public async Task GetByIdAsync(Guid id) { await using DbConnection connection = await CreateConnection(); @@ -47,10 +48,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) return await reader.ReadAsync() ? MapToEntity(reader) : null; } - /// - /// Retrieves all brewery posts, optionally paginated, ordered by creation date descending. Posts - /// without a location are included, with left null. - /// + /// public async Task> GetAllAsync(int? limit, int? offset) { await using DbConnection connection = await CreateConnection(); @@ -75,15 +73,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) return breweries; } - /// - /// Updates a brewery post's name and description, and upserts or clears its location. When - /// .Location is null, any existing location for the - /// brewery is removed. - /// - /// Thrown when the brewery, or the given city, does not exist. - /// - /// Thrown when 's Timer no longer matches the stored row. - /// + /// public async Task UpdateAsync(BreweryPost brewery) { await using DbConnection connection = await CreateConnection(); @@ -221,10 +211,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) ); } - /// - /// Deletes a brewery post by ID. Its location and photos are removed via cascading foreign keys. - /// - /// Thrown when no brewery exists with the given . + /// public async Task DeleteAsync(Guid id) { await using DbConnection connection = await CreateConnection(); @@ -237,11 +224,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory) throw new NotFoundException("Brewery not found."); } - /// Creates a new brewery post, including its location details. - /// Thrown when has no Location. - /// - /// Thrown when .PostedById or Location.CityId does not exist. - /// + /// public async Task CreateAsync(BreweryPost brewery) { if (brewery.Location is null) diff --git a/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs index cbd14b03..a56f525d 100644 --- a/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs +++ b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs @@ -7,14 +7,25 @@ namespace Features.Breweries.Repository; /// public interface IBreweryRepository { - /// Returns null if no brewery post exists with the given ID. + /// + /// Retrieves a brewery post by ID, joined to its location. Returns null if no brewery post + /// exists with the given ID, or if it has no associated location. + /// Task GetByIdAsync(Guid id); + /// + /// Retrieves all brewery posts, optionally paginated, ordered by creation date descending. Posts + /// without a location are included, with left null. + /// + /// Maximum number of rows to return. Unbounded if . + /// Number of rows to skip. Treated as zero if . Task> GetAllAsync(int? limit, int? offset); /// - /// Updates a brewery post, enforcing optimistic concurrency via 's - /// Timer: the update is rejected if the row was modified since Timer was read. + /// Updates a brewery post's name and description, and upserts or clears its location, enforcing + /// optimistic concurrency via 's Timer: the update is rejected + /// if the row was modified since Timer was read. When 's + /// Location is null, any existing location for the brewery is removed. /// Returns the freshly persisted brewery, including its new Timer. /// /// @@ -27,9 +38,14 @@ public interface IBreweryRepository /// Task UpdateAsync(BreweryPost brewery); + /// Deletes a brewery post by ID. Its location and photos are removed via cascading foreign keys. + /// Thrown when no brewery exists with the given . Task DeleteAsync(Guid id); /// Creates a new brewery post, including its location details. /// Thrown when has no Location. + /// + /// Thrown when 's PostedById or Location.CityId does not exist. + /// Task CreateAsync(BreweryPost brewery); } diff --git a/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj b/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj index 11921d56..5944af3e 100644 --- a/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj +++ b/web/backend/Features/Features.Emails.Tests/Features.Emails.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs b/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs index 5a64d9db..6459e773 100644 --- a/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Emails/DependencyInjection/FeaturesEmailsServiceCollectionExtensions.cs @@ -5,8 +5,15 @@ using Microsoft.Extensions.DependencyInjection; namespace Features.Emails.DependencyInjection; +/// +/// Registers the services used by the Features.Emails slice. +/// public static class FeaturesEmailsServiceCollectionExtensions { + /// + /// Registers the SMTP email provider, template renderer, and . + /// + /// The same instance, for chaining. public static IServiceCollection AddFeaturesEmails(this IServiceCollection services) { services.AddScoped(); diff --git a/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs b/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs index 01c42c37..23bea801 100644 --- a/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs +++ b/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs @@ -22,6 +22,7 @@ public class EmailDispatcher( Environment.GetEnvironmentVariable("WEBSITE_BASE_URL") ?? throw new InvalidOperationException("WEBSITE_BASE_URL environment variable is not set"); + /// public async Task SendRegistrationEmailAsync( string firstName, string email, @@ -38,6 +39,7 @@ public class EmailDispatcher( await emailProvider.SendAsync(email, "Welcome to The Biergarten App!", emailHtml, true); } + /// public async Task SendResendConfirmationEmailAsync( string firstName, string email, diff --git a/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs b/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs index 686a98a6..7116696a 100644 --- a/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs +++ b/web/backend/Features/Features.Emails/Services/IEmailDispatcher.cs @@ -8,10 +8,14 @@ public interface IEmailDispatcher /// /// Sends a welcome email containing an account confirmation link to a newly registered user. /// + /// The email is sent synchronously; the returned task completes once delivery to the mail server succeeds or fails. + /// Embedded in the confirmation link sent to . Task SendRegistrationEmailAsync(string firstName, string email, string confirmationToken); /// /// Sends an email containing a fresh account confirmation link to a user who requested a resend. /// + /// The email is sent synchronously; the returned task completes once delivery to the mail server succeeds or fails. + /// Embedded in the confirmation link sent to . Task SendResendConfirmationEmailAsync(string firstName, string email, string confirmationToken); } diff --git a/web/backend/Features/Features.Locations/DependencyInjection/FeaturesLocationsServiceCollectionExtensions.cs b/web/backend/Features/Features.Locations/DependencyInjection/FeaturesLocationsServiceCollectionExtensions.cs index 259b4d07..5d412a62 100644 --- a/web/backend/Features/Features.Locations/DependencyInjection/FeaturesLocationsServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Locations/DependencyInjection/FeaturesLocationsServiceCollectionExtensions.cs @@ -3,8 +3,15 @@ using Microsoft.Extensions.DependencyInjection; namespace Features.Locations.DependencyInjection; +/// +/// Registers the services used by the Features.Locations slice. +/// public static class FeaturesLocationsServiceCollectionExtensions { + /// + /// Registers and its Dapper-based implementation. + /// + /// The same instance, for chaining. public static IServiceCollection AddFeaturesLocations(this IServiceCollection services) { services.AddScoped(); diff --git a/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs b/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs index 4967b127..1daae7af 100644 --- a/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs +++ b/web/backend/Features/Features.UserManagement/Commands/UpdateUser/UpdateUserHandler.cs @@ -3,6 +3,9 @@ using MediatR; namespace Features.UserManagement.Commands.UpdateUser; +/// +/// Handles . +/// public class UpdateUserHandler(IUserAccountRepository repository) : IRequestHandler { diff --git a/web/backend/Features/Features.UserManagement/Controllers/UserController.cs b/web/backend/Features/Features.UserManagement/Controllers/UserController.cs index 1bd98ce7..669d4f5e 100644 --- a/web/backend/Features/Features.UserManagement/Controllers/UserController.cs +++ b/web/backend/Features/Features.UserManagement/Controllers/UserController.cs @@ -1,4 +1,5 @@ using Domain.Entities; +using Domain.Exceptions; using Features.UserManagement.Queries.GetAllUsers; using Features.UserManagement.Queries.GetUserById; using MediatR; @@ -6,10 +7,14 @@ using Microsoft.AspNetCore.Mvc; namespace Features.UserManagement.Controllers; +/// +/// Exposes read endpoints for user accounts. +/// [ApiController] [Route("api/[controller]")] public class UserController(IMediator mediator) : ControllerBase { + /// Gets a page of user accounts, ordered by creation date descending. /// null for no limit. /// null for no offset. [HttpGet] @@ -22,6 +27,10 @@ public class UserController(IMediator mediator) : ControllerBase return Ok(users); } + /// Gets the user account with the given ID. + /// + /// Thrown when no user account exists with the given ID, resulting in a 404 response. + /// [HttpGet("{id:guid}")] public async Task> GetById(Guid id) { diff --git a/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs b/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs index ebd3c081..caff349d 100644 --- a/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.UserManagement/DependencyInjection/FeaturesUserManagementServiceCollectionExtensions.cs @@ -3,8 +3,15 @@ using Microsoft.Extensions.DependencyInjection; namespace Features.UserManagement.DependencyInjection; +/// +/// Registers the services used by the Features.UserManagement slice. +/// public static class FeaturesUserManagementServiceCollectionExtensions { + /// + /// Registers and its Dapper-based implementation. + /// + /// The same instance, for chaining. public static IServiceCollection AddFeaturesUserManagement(this IServiceCollection services) { services.AddScoped(); diff --git a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs index 4e8afe82..bc0822ec 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersHandler.cs @@ -4,6 +4,9 @@ using MediatR; namespace Features.UserManagement.Queries.GetAllUsers; +/// +/// Handles . +/// public class GetAllUsersHandler(IUserAccountRepository repository) : IRequestHandler> { diff --git a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs index 90adf926..54e8d4cf 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetAllUsers/GetAllUsersQuery.cs @@ -3,4 +3,9 @@ using MediatR; namespace Features.UserManagement.Queries.GetAllUsers; +/// +/// Requests a page of user accounts, ordered by creation date descending. +/// +/// null for no limit. +/// null for no offset. public record GetAllUsersQuery(int? Limit, int? Offset) : IRequest>; diff --git a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs index d664310e..bab1687e 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdHandler.cs @@ -5,6 +5,9 @@ using MediatR; namespace Features.UserManagement.Queries.GetUserById; +/// +/// Handles . +/// public class GetUserByIdHandler(IUserAccountRepository repository) : IRequestHandler { diff --git a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs index 8337b7a9..51d47c2f 100644 --- a/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs +++ b/web/backend/Features/Features.UserManagement/Queries/GetUserById/GetUserByIdQuery.cs @@ -3,4 +3,7 @@ using MediatR; namespace Features.UserManagement.Queries.GetUserById; +/// +/// Requests the user account with the given ID. +/// public record GetUserByIdQuery(Guid UserAccountId) : IRequest; diff --git a/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs b/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs index e7c684c6..25ebf13d 100644 --- a/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs +++ b/web/backend/Features/Features.UserManagement/Repository/IUserAccountRepository.cs @@ -1,4 +1,5 @@ using Domain.Entities; +using Domain.Exceptions; namespace Features.UserManagement.Repository; @@ -10,13 +11,18 @@ public interface IUserAccountRepository /// Returns null if no user account exists with the given ID. Task GetByIdAsync(Guid id); + /// Returns user accounts ordered by creation date descending. /// null for no limit. /// null for no offset. Task> GetAllAsync(int? limit, int? offset); + /// Updates the mutable fields of an existing user account. /// Must have a valid UserAccountId. + /// Thrown when no user account exists with the given ID. Task UpdateAsync(UserAccount userAccount); + /// Deletes the user account with the given ID. + /// Thrown when no user account exists with the given ID. Task DeleteAsync(Guid id); /// Returns null if no user account exists with the given username. diff --git a/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs b/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs index 63935694..c911007e 100644 --- a/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs +++ b/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs @@ -46,7 +46,6 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) } /// - /// Thrown when no user account exists with the given ID. public async Task UpdateAsync(UserAccount userAccount) { await using DbConnection connection = await CreateConnection(); @@ -73,7 +72,6 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) } /// - /// Thrown when no user account exists with the given . public async Task DeleteAsync(Guid id) { await using DbConnection connection = await CreateConnection(); diff --git a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs index 332fc582..ed29bf30 100644 --- a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/EmailTemplateProvider.cs @@ -12,6 +12,7 @@ namespace Infrastructure.Email.Templates.Rendering; public class EmailTemplateProvider(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : IEmailTemplateProvider { + /// public async Task RenderUserRegisteredEmailAsync( string username, string confirmationLink @@ -26,6 +27,7 @@ public class EmailTemplateProvider(IServiceProvider serviceProvider, ILoggerFact return await RenderComponentAsync(parameters); } + /// public async Task RenderResendConfirmationEmailAsync( string username, string confirmationLink diff --git a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs index 2fd75271..b3929973 100644 --- a/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email.Templates/Rendering/IEmailTemplateProvider.cs @@ -5,13 +5,15 @@ namespace Infrastructure.Email.Templates.Rendering; /// public interface IEmailTemplateProvider { - /// - /// Renders the UserRegisteredEmail template. - /// + /// Renders the registration confirmation email template. + /// The recipient's username, shown in the greeting. + /// The URL the recipient follows to confirm their account. + /// A task whose result is the rendered HTML for the email body. Task RenderUserRegisteredEmailAsync(string username, string confirmationLink); - /// - /// Renders the ResendConfirmation template. - /// + /// Renders the resend-confirmation email template. + /// The recipient's username, shown in the greeting. + /// The URL the recipient follows to confirm their account. + /// A task whose result is the rendered HTML for the email body. Task RenderResendConfirmationEmailAsync(string username, string confirmationLink); } diff --git a/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs b/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs index 6a9f3f3c..94641d2c 100644 --- a/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email/IEmailProvider.cs @@ -5,7 +5,17 @@ namespace Infrastructure.Email; /// public interface IEmailProvider { + /// Sends an email to a single recipient. + /// The recipient's email address. + /// The email subject line. + /// The email body content. + /// Whether is HTML; when it is sent as plain text. Task SendAsync(string to, string subject, string body, bool isHtml = true); + /// Sends an email to multiple recipients. + /// The recipients' email addresses. + /// The email subject line. + /// The email body content. + /// Whether is HTML; when it is sent as plain text. Task SendAsync(IEnumerable to, string subject, string body, bool isHtml = true); } diff --git a/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs b/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs index 71b67b34..4928ca16 100644 --- a/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs +++ b/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs @@ -51,15 +51,17 @@ public class SmtpEmailProvider : IEmailProvider _fromName = Environment.GetEnvironmentVariable("SMTP_FROM_NAME") ?? "The Biergarten"; } + /// public async Task SendAsync(string to, string subject, string body, bool isHtml = true) { await SendAsync([to], subject, body, isHtml); } - /// + /// + /// /// Connects using StartTls when SSL is enabled (or no encryption otherwise), authenticates /// if credentials were configured, sends the message, and disconnects. - /// + /// /// /// Thrown when connecting, authenticating, or sending via SMTP fails, wrapping the original exception. /// diff --git a/web/backend/Infrastructure/Infrastructure.Sql/DependencyInjection/InfrastructureSqlServiceCollectionExtensions.cs b/web/backend/Infrastructure/Infrastructure.Sql/DependencyInjection/InfrastructureSqlServiceCollectionExtensions.cs index 0c867d4e..6573fe5b 100644 --- a/web/backend/Infrastructure/Infrastructure.Sql/DependencyInjection/InfrastructureSqlServiceCollectionExtensions.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/DependencyInjection/InfrastructureSqlServiceCollectionExtensions.cs @@ -2,8 +2,11 @@ using Microsoft.Extensions.DependencyInjection; namespace Infrastructure.Sql.DependencyInjection; +/// Extension methods for registering SQL infrastructure services with dependency injection. public static class InfrastructureSqlServiceCollectionExtensions { + /// Registers as a singleton . + /// The same instance, for chaining. public static IServiceCollection AddInfrastructureSql(this IServiceCollection services) { services.AddSingleton(); diff --git a/web/backend/Infrastructure/Infrastructure.Sql/SqlErrorCodes.cs b/web/backend/Infrastructure/Infrastructure.Sql/SqlErrorCodes.cs index bdf3c965..1dece02f 100644 --- a/web/backend/Infrastructure/Infrastructure.Sql/SqlErrorCodes.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/SqlErrorCodes.cs @@ -1,15 +1,18 @@ namespace Infrastructure.Sql; /// -/// Custom SQL Server error numbers (50000+) that this application's stored procedures use to -/// signal domain-level failures via THROW. Repositories catch -/// and switch on ex.Number against -/// these constants to translate the failure into the corresponding Domain.Exceptions type. +/// Custom SQL Server error numbers (50000+) that a stored procedure could use to signal +/// domain-level failures via THROW, for a repository to catch as +/// and translate via ex.Number into +/// the corresponding Domain.Exceptions type. /// /// -/// Each number is shared by every procedure that raises that category of error — it identifies -/// the category, not a single call site. The procedure's own error message (ex.Message) -/// already says which entity or rule was involved, so it is safe to surface directly to callers. +/// Currently unused: the schema has no stored procedures, and no repository throws or catches on +/// these numbers. Repositories instead run their own existence/concurrency checks and throw +/// Domain.Exceptions directly (see docs/website/database.md for the current +/// approach). This type is kept in case a future stored procedure reintroduces the pattern; each +/// number would +/// identify a category of error shared by every procedure that raises it, not a single call site. /// public static class SqlErrorCodes { diff --git a/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs b/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs index 05a21fcb..aabc964f 100644 --- a/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs +++ b/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs @@ -12,6 +12,7 @@ public class ValidationBehavior(IEnumerable where TRequest : IRequest { + /// public async Task Handle( TRequest request, RequestHandlerDelegate next, diff --git a/web/backend/Shared/Shared.Contracts/ResponseBody.cs b/web/backend/Shared/Shared.Contracts/ResponseBody.cs index 2326814a..d3f4e690 100644 --- a/web/backend/Shared/Shared.Contracts/ResponseBody.cs +++ b/web/backend/Shared/Shared.Contracts/ResponseBody.cs @@ -5,8 +5,10 @@ namespace Shared.Contracts; /// public record ResponseBody { + /// Gets the human-readable message describing the response. public required string Message { get; init; } + /// Gets the response's data payload. public required T Payload { get; init; } } @@ -15,5 +17,6 @@ public record ResponseBody /// public record ResponseBody { + /// Gets the human-readable message describing the response. public required string Message { get; init; } }