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; }
}