mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
add xmldoc comments
This commit is contained in:
@@ -5,12 +5,28 @@ using Service.Emails;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Handles confirmation of newly registered user accounts and resending of confirmation emails.
|
||||
/// </summary>
|
||||
/// <param name="authRepository">Repository used to look up and confirm user accounts.</param>
|
||||
/// <param name="tokenService">Service used to generate and validate confirmation tokens.</param>
|
||||
/// <param name="emailService">Service used to send confirmation-related emails.</param>
|
||||
public class ConfirmationService(
|
||||
IAuthRepository authRepository,
|
||||
ITokenService tokenService,
|
||||
IEmailService emailService
|
||||
) : IConfirmationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates a confirmation token and marks the corresponding user account as confirmed.
|
||||
/// </summary>
|
||||
/// <param name="confirmationToken">The confirmation token issued to the user, typically delivered via email.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="ConfirmationServiceReturn"/> containing the UTC timestamp of confirmation and the confirmed user's ID.
|
||||
/// </returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when the confirmation token is invalid or expired, or when the associated user account cannot be found.
|
||||
/// </exception>
|
||||
public async Task<ConfirmationServiceReturn> ConfirmUserAsync(
|
||||
string confirmationToken
|
||||
)
|
||||
@@ -34,6 +50,15 @@ public class ConfirmationService(
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resends the account confirmation email to a user, generating a fresh confirmation token.
|
||||
/// </summary>
|
||||
/// <param name="userId">The unique identifier of the user requesting the resend.</param>
|
||||
/// <returns>A task that completes once the operation has finished.</returns>
|
||||
/// <remarks>
|
||||
/// Returns silently without sending an email if the user does not exist (to prevent user enumeration)
|
||||
/// or if the user's account is already verified.
|
||||
/// </remarks>
|
||||
public async Task ResendConfirmationEmailAsync(Guid userId)
|
||||
{
|
||||
var user = await authRepository.GetUserByIdAsync(userId);
|
||||
|
||||
@@ -3,11 +3,30 @@ using Infrastructure.Repository.Auth;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of successfully confirming a user account.
|
||||
/// </summary>
|
||||
/// <param name="ConfirmedAt">The UTC date and time at which the account was confirmed.</param>
|
||||
/// <param name="UserId">The unique identifier of the confirmed user.</param>
|
||||
public record ConfirmationServiceReturn(DateTime ConfirmedAt, Guid UserId);
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for confirming user accounts and resending confirmation emails.
|
||||
/// </summary>
|
||||
public interface IConfirmationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates a confirmation token and confirms the corresponding user account.
|
||||
/// </summary>
|
||||
/// <param name="confirmationToken">The confirmation token issued to the user.</param>
|
||||
/// <returns>A <see cref="ConfirmationServiceReturn"/> describing the confirmed account.</returns>
|
||||
Task<ConfirmationServiceReturn> ConfirmUserAsync(string confirmationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Resends the account confirmation email for the specified user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The unique identifier of the user requesting the resend.</param>
|
||||
/// <returns>A task that completes once the operation has finished.</returns>
|
||||
Task ResendConfirmationEmailAsync(Guid userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,12 +2,28 @@ using Domain.Entities;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a successful login attempt.
|
||||
/// </summary>
|
||||
/// <param name="UserAccount">The authenticated user's account.</param>
|
||||
/// <param name="RefreshToken">The issued refresh token.</param>
|
||||
/// <param name="AccessToken">The issued access token.</param>
|
||||
public record LoginServiceReturn(
|
||||
UserAccount UserAccount,
|
||||
string RefreshToken,
|
||||
string AccessToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the operation for authenticating a user with a username and password.
|
||||
/// </summary>
|
||||
public interface ILoginService
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticates a user using their username and password and issues new tokens.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the account to authenticate.</param>
|
||||
/// <param name="password">The plain-text password to verify against the stored credential.</param>
|
||||
/// <returns>A <see cref="LoginServiceReturn"/> containing the authenticated user and issued tokens.</returns>
|
||||
Task<LoginServiceReturn> LoginAsync(string username, string password);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,45 @@ using Domain.Entities;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a user registration attempt.
|
||||
/// </summary>
|
||||
public record RegisterServiceReturn
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether tokens were issued for the newly created account.
|
||||
/// <c>false</c> when token generation failed, in which case <see cref="AccessToken"/> and
|
||||
/// <see cref="RefreshToken"/> are empty.
|
||||
/// </summary>
|
||||
public bool IsAuthenticated { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the registration confirmation email was sent successfully.
|
||||
/// </summary>
|
||||
public bool EmailSent { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user account that was created.
|
||||
/// </summary>
|
||||
public UserAccount UserAccount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the issued access token, or an empty string if authentication was not completed.
|
||||
/// </summary>
|
||||
public string AccessToken { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the issued refresh token, or an empty string if authentication was not completed.
|
||||
/// </summary>
|
||||
public string RefreshToken { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance representing a successful registration with issued tokens.
|
||||
/// </summary>
|
||||
/// <param name="userAccount">The newly created user account.</param>
|
||||
/// <param name="accessToken">The issued access token.</param>
|
||||
/// <param name="refreshToken">The issued refresh token.</param>
|
||||
/// <param name="emailSent">Whether the confirmation email was sent successfully.</param>
|
||||
public RegisterServiceReturn(
|
||||
UserAccount userAccount,
|
||||
string accessToken,
|
||||
@@ -24,14 +55,27 @@ public record RegisterServiceReturn
|
||||
RefreshToken = refreshToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance representing a registration where tokens could not be issued.
|
||||
/// </summary>
|
||||
/// <param name="userAccount">The newly created user account.</param>
|
||||
public RegisterServiceReturn(UserAccount userAccount)
|
||||
{
|
||||
UserAccount = userAccount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the operation for registering a new user account.
|
||||
/// </summary>
|
||||
public interface IRegisterService
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a new user account with the given password.
|
||||
/// </summary>
|
||||
/// <param name="userAccount">The user account details to register.</param>
|
||||
/// <param name="password">The plain-text password to hash and store for the new account.</param>
|
||||
/// <returns>A <see cref="RegisterServiceReturn"/> describing the outcome of the registration.</returns>
|
||||
Task<RegisterServiceReturn> RegisterAsync(
|
||||
UserAccount userAccount,
|
||||
string password
|
||||
|
||||
@@ -7,40 +7,119 @@ using Infrastructure.Repository.Auth;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the kind of token being generated or validated.
|
||||
/// </summary>
|
||||
public enum TokenType
|
||||
{
|
||||
/// <summary>A short-lived token used to authorize API requests.</summary>
|
||||
AccessToken,
|
||||
/// <summary>A long-lived token used to obtain new access tokens.</summary>
|
||||
RefreshToken,
|
||||
/// <summary>A short-lived token used to confirm a user's email/account.</summary>
|
||||
ConfirmationToken,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of successfully validating a token.
|
||||
/// </summary>
|
||||
/// <param name="UserId">The unique identifier of the user the token belongs to.</param>
|
||||
/// <param name="Username">The username extracted from the token's claims.</param>
|
||||
/// <param name="Principal">The <see cref="ClaimsPrincipal"/> produced from the validated token.</param>
|
||||
public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of refreshing a user's session.
|
||||
/// </summary>
|
||||
/// <param name="UserAccount">The user account associated with the refreshed session.</param>
|
||||
/// <param name="RefreshToken">The newly issued refresh token.</param>
|
||||
/// <param name="AccessToken">The newly issued access token.</param>
|
||||
public record RefreshTokenResult(
|
||||
UserAccount UserAccount,
|
||||
string RefreshToken,
|
||||
string AccessToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the expiration windows, in hours, for each type of token issued by <see cref="ITokenService"/>.
|
||||
/// </summary>
|
||||
public static class TokenServiceExpirationHours
|
||||
{
|
||||
/// <summary>The expiration window, in hours, for access tokens.</summary>
|
||||
public const double AccessTokenHours = 1;
|
||||
/// <summary>The expiration window, in hours, for refresh tokens (21 days).</summary>
|
||||
public const double RefreshTokenHours = 504; // 21 days
|
||||
/// <summary>The expiration window, in hours, for confirmation tokens (30 minutes).</summary>
|
||||
public const double ConfirmationTokenHours = 0.5; // 30 minutes
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for generating and validating JWTs used for access, refresh, and account confirmation.
|
||||
/// </summary>
|
||||
public interface ITokenService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a new access token for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed access token string.</returns>
|
||||
string GenerateAccessToken(UserAccount user);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new refresh token for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed refresh token string.</returns>
|
||||
string GenerateRefreshToken(UserAccount user);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new confirmation token for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed confirmation token string.</returns>
|
||||
string GenerateConfirmationToken(UserAccount user);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a token of the type specified by <typeparamref name="T"/>, which must be <see cref="TokenType"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type identifying which kind of token to generate. Must be <see cref="TokenType"/>.</typeparam>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed token string corresponding to the requested token type.</returns>
|
||||
string GenerateToken<T>(UserAccount user) where T : struct, Enum;
|
||||
|
||||
/// <summary>
|
||||
/// Validates an access token.
|
||||
/// </summary>
|
||||
/// <param name="token">The access token string to validate.</param>
|
||||
/// <returns>A <see cref="ValidatedToken"/> describing the token's claims.</returns>
|
||||
Task<ValidatedToken> ValidateAccessTokenAsync(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a refresh token.
|
||||
/// </summary>
|
||||
/// <param name="token">The refresh token string to validate.</param>
|
||||
/// <returns>A <see cref="ValidatedToken"/> describing the token's claims.</returns>
|
||||
Task<ValidatedToken> ValidateRefreshTokenAsync(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a confirmation token.
|
||||
/// </summary>
|
||||
/// <param name="token">The confirmation token string to validate.</param>
|
||||
/// <returns>A <see cref="ValidatedToken"/> describing the token's claims.</returns>
|
||||
Task<ValidatedToken> ValidateConfirmationTokenAsync(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a refresh token and issues a new access/refresh token pair for the associated user.
|
||||
/// </summary>
|
||||
/// <param name="refreshTokenString">The refresh token string to validate and exchange.</param>
|
||||
/// <returns>A <see cref="RefreshTokenResult"/> containing the user and newly issued tokens.</returns>
|
||||
Task<RefreshTokenResult> RefreshTokenAsync(string refreshTokenString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="ITokenService"/> that generates and validates JWTs
|
||||
/// for access, refresh, and confirmation flows using secrets read from environment variables.
|
||||
/// </summary>
|
||||
public class TokenService : ITokenService
|
||||
{
|
||||
private readonly ITokenInfrastructure _tokenInfrastructure;
|
||||
@@ -50,6 +129,16 @@ public class TokenService : ITokenService
|
||||
private readonly string _refreshTokenSecret;
|
||||
private readonly string _confirmationTokenSecret;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="TokenService"/>, loading the access, refresh, and
|
||||
/// confirmation token signing secrets from environment variables.
|
||||
/// </summary>
|
||||
/// <param name="tokenInfrastructure">The infrastructure component used to generate and validate JWTs.</param>
|
||||
/// <param name="authRepository">Repository used to look up user accounts during token refresh.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when any of the <c>ACCESS_TOKEN_SECRET</c>, <c>REFRESH_TOKEN_SECRET</c>, or
|
||||
/// <c>CONFIRMATION_TOKEN_SECRET</c> environment variables are not set.
|
||||
/// </exception>
|
||||
public TokenService(
|
||||
ITokenInfrastructure tokenInfrastructure,
|
||||
IAuthRepository authRepository
|
||||
@@ -68,24 +157,53 @@ public class TokenService : ITokenService
|
||||
?? throw new InvalidOperationException("CONFIRMATION_TOKEN_SECRET environment variable is not set");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an access token for the given user, signed with the access token secret and
|
||||
/// expiring after <see cref="TokenServiceExpirationHours.AccessTokenHours"/> hours.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed access token string.</returns>
|
||||
public string GenerateAccessToken(UserAccount user)
|
||||
{
|
||||
var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours);
|
||||
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _accessTokenSecret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a refresh token for the given user, signed with the refresh token secret and
|
||||
/// expiring after <see cref="TokenServiceExpirationHours.RefreshTokenHours"/> hours.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed refresh token string.</returns>
|
||||
public string GenerateRefreshToken(UserAccount user)
|
||||
{
|
||||
var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.RefreshTokenHours);
|
||||
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _refreshTokenSecret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a confirmation token for the given user, signed with the confirmation token secret and
|
||||
/// expiring after <see cref="TokenServiceExpirationHours.ConfirmationTokenHours"/> hours.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed confirmation token string.</returns>
|
||||
public string GenerateConfirmationToken(UserAccount user)
|
||||
{
|
||||
var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.ConfirmationTokenHours);
|
||||
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _confirmationTokenSecret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a token of the kind specified by <typeparamref name="T"/>, dispatching to
|
||||
/// <see cref="GenerateAccessToken"/>, <see cref="GenerateRefreshToken"/>, or
|
||||
/// <see cref="GenerateConfirmationToken"/> based on the corresponding <see cref="TokenType"/> value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type identifying which kind of token to generate. Must be <see cref="TokenType"/>.</typeparam>
|
||||
/// <param name="user">The user account to generate the token for.</param>
|
||||
/// <returns>The signed token string corresponding to the requested token type.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when <typeparamref name="T"/> is not <see cref="TokenType"/> or does not resolve to a known token type.
|
||||
/// </exception>
|
||||
public string GenerateToken<T>(UserAccount user) where T : struct, Enum
|
||||
{
|
||||
if (typeof(T) != typeof(TokenType))
|
||||
@@ -105,15 +223,51 @@ public class TokenService : ITokenService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an access token against the access token secret.
|
||||
/// </summary>
|
||||
/// <param name="token">The access token string to validate.</param>
|
||||
/// <returns>A <see cref="ValidatedToken"/> describing the token's claims.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
|
||||
/// </exception>
|
||||
public async Task<ValidatedToken> ValidateAccessTokenAsync(string token)
|
||||
=> await ValidateTokenInternalAsync(token, _accessTokenSecret, "access");
|
||||
|
||||
/// <summary>
|
||||
/// Validates a refresh token against the refresh token secret.
|
||||
/// </summary>
|
||||
/// <param name="token">The refresh token string to validate.</param>
|
||||
/// <returns>A <see cref="ValidatedToken"/> describing the token's claims.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
|
||||
/// </exception>
|
||||
public async Task<ValidatedToken> ValidateRefreshTokenAsync(string token)
|
||||
=> await ValidateTokenInternalAsync(token, _refreshTokenSecret, "refresh");
|
||||
|
||||
/// <summary>
|
||||
/// Validates a confirmation token against the confirmation token secret.
|
||||
/// </summary>
|
||||
/// <param name="token">The confirmation token string to validate.</param>
|
||||
/// <returns>A <see cref="ValidatedToken"/> describing the token's claims.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
|
||||
/// </exception>
|
||||
public async Task<ValidatedToken> ValidateConfirmationTokenAsync(string token)
|
||||
=> await ValidateTokenInternalAsync(token, _confirmationTokenSecret, "confirmation");
|
||||
|
||||
/// <summary>
|
||||
/// Performs the shared validation logic for access, refresh, and confirmation tokens:
|
||||
/// validates the JWT signature/expiration, then extracts and parses the user ID and username claims.
|
||||
/// </summary>
|
||||
/// <param name="token">The token string to validate.</param>
|
||||
/// <param name="secret">The secret key to validate the token's signature against.</param>
|
||||
/// <param name="tokenType">A human-readable label (e.g. "access", "refresh", "confirmation") used in error messages.</param>
|
||||
/// <returns>A <see cref="ValidatedToken"/> describing the token's claims.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when required claims are missing, the user ID claim is not a valid <see cref="Guid"/>,
|
||||
/// or the underlying token validation fails for any other reason (e.g. expired or invalid signature).
|
||||
/// </exception>
|
||||
private async Task<ValidatedToken> ValidateTokenInternalAsync(string token, string secret, string tokenType)
|
||||
{
|
||||
try
|
||||
@@ -141,6 +295,15 @@ public class TokenService : ITokenService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the given refresh token, looks up the associated user, and issues a fresh
|
||||
/// access/refresh token pair.
|
||||
/// </summary>
|
||||
/// <param name="refreshTokenString">The refresh token string to validate and exchange.</param>
|
||||
/// <returns>A <see cref="RefreshTokenResult"/> containing the user and newly issued tokens.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when the refresh token is invalid, or when the user account it refers to no longer exists.
|
||||
/// </exception>
|
||||
public async Task<RefreshTokenResult> RefreshTokenAsync(string refreshTokenString)
|
||||
{
|
||||
var validated = await ValidateRefreshTokenAsync(refreshTokenString);
|
||||
|
||||
@@ -6,12 +6,28 @@ using Infrastructure.Repository.Auth;
|
||||
namespace Service.Auth;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles authenticating users by verifying their credentials and issuing access/refresh tokens.
|
||||
/// </summary>
|
||||
/// <param name="authRepo">Repository used to look up user accounts and their active credentials.</param>
|
||||
/// <param name="passwordInfrastructure">Infrastructure component used to verify a plain-text password against a stored hash.</param>
|
||||
/// <param name="tokenService">Service used to generate access and refresh tokens for an authenticated user.</param>
|
||||
public class LoginService(
|
||||
IAuthRepository authRepo,
|
||||
IPasswordInfrastructure passwordInfrastructure,
|
||||
ITokenService tokenService
|
||||
) : ILoginService
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticates a user by username and password, issuing a new access and refresh token on success.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the account to authenticate.</param>
|
||||
/// <param name="password">The plain-text password to verify against the stored credential.</param>
|
||||
/// <returns>A <see cref="LoginServiceReturn"/> containing the authenticated user and issued tokens.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when the username does not match any account, the account has no active credential,
|
||||
/// or the supplied password does not match the stored hash.
|
||||
/// </exception>
|
||||
public async Task<LoginServiceReturn> LoginAsync(
|
||||
string username,
|
||||
string password
|
||||
|
||||
@@ -9,6 +9,14 @@ using Service.Emails;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Handles registration of new user accounts, including uniqueness validation, password hashing,
|
||||
/// token issuance, and sending the registration confirmation email.
|
||||
/// </summary>
|
||||
/// <param name="authRepo">Repository used to check for existing users and persist the new account.</param>
|
||||
/// <param name="passwordInfrastructure">Infrastructure component used to hash the user's plain-text password.</param>
|
||||
/// <param name="tokenService">Service used to generate access, refresh, and confirmation tokens.</param>
|
||||
/// <param name="emailService">Service used to send the registration confirmation email.</param>
|
||||
public class RegisterService(
|
||||
IAuthRepository authRepo,
|
||||
IPasswordInfrastructure passwordInfrastructure,
|
||||
@@ -16,6 +24,13 @@ public class RegisterService(
|
||||
IEmailService emailService
|
||||
) : IRegisterService
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that no existing user account has the same username or email as the given account.
|
||||
/// </summary>
|
||||
/// <param name="userAccount">The candidate user account whose username and email should be checked for uniqueness.</param>
|
||||
/// <exception cref="Domain.Exceptions.ConflictException">
|
||||
/// Thrown when an existing account already has the same username or email address.
|
||||
/// </exception>
|
||||
private async Task ValidateUserDoesNotExist(UserAccount userAccount)
|
||||
{
|
||||
// Check if user already exists
|
||||
@@ -32,6 +47,21 @@ public class RegisterService(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new user account: validates uniqueness, hashes the password, persists the account,
|
||||
/// issues access/refresh/confirmation tokens, and attempts to send the registration confirmation email.
|
||||
/// </summary>
|
||||
/// <param name="userAccount">The user account details to register.</param>
|
||||
/// <param name="password">The plain-text password to hash and store for the new account.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="RegisterServiceReturn"/> describing the outcome. If token generation fails, the
|
||||
/// returned value has <see cref="RegisterServiceReturn.IsAuthenticated"/> set to <c>false</c> and no tokens.
|
||||
/// Otherwise tokens are populated and <see cref="RegisterServiceReturn.EmailSent"/> reflects whether the
|
||||
/// confirmation email was sent successfully (failures to send the email are swallowed and do not fail registration).
|
||||
/// </returns>
|
||||
/// <exception cref="Domain.Exceptions.ConflictException">
|
||||
/// Thrown when an existing account already has the same username or email address.
|
||||
/// </exception>
|
||||
public async Task<RegisterServiceReturn> RegisterAsync(
|
||||
UserAccount userAccount,
|
||||
string password
|
||||
|
||||
@@ -3,14 +3,34 @@ using Infrastructure.Repository.Breweries;
|
||||
|
||||
namespace Service.Breweries;
|
||||
|
||||
/// <summary>
|
||||
/// Handles retrieval, creation, update, and deletion of brewery posts.
|
||||
/// </summary>
|
||||
/// <param name="repository">Repository used to persist and query brewery post data.</param>
|
||||
public class BreweryService(IBreweryRepository repository) : IBreweryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a brewery post by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the brewery post.</param>
|
||||
/// <returns>The matching <see cref="BreweryPost"/>, or <c>null</c> if no such post exists.</returns>
|
||||
public Task<BreweryPost?> GetByIdAsync(Guid id) =>
|
||||
repository.GetByIdAsync(id);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all brewery posts, optionally paginated.
|
||||
/// </summary>
|
||||
/// <param name="limit">The maximum number of results to return, or <c>null</c> for no limit.</param>
|
||||
/// <param name="offset">The number of results to skip, or <c>null</c> to start from the beginning.</param>
|
||||
/// <returns>A collection of <see cref="BreweryPost"/> entities.</returns>
|
||||
public Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit = null, int? offset = null) =>
|
||||
repository.GetAllAsync(limit, offset);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new brewery post, generating new identifiers for the post and its location.
|
||||
/// </summary>
|
||||
/// <param name="request">The details of the brewery post to create.</param>
|
||||
/// <returns>A successful <see cref="BreweryServiceReturn"/> wrapping the newly created brewery post.</returns>
|
||||
public async Task<BreweryServiceReturn> CreateAsync(BreweryCreateRequest request)
|
||||
{
|
||||
var entity = new BreweryPost
|
||||
@@ -35,6 +55,12 @@ public class BreweryService(IBreweryRepository repository) : IBreweryService
|
||||
return new BreweryServiceReturn(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing brewery post. If <paramref name="request"/> has no <c>Location</c>,
|
||||
/// the brewery's location is cleared.
|
||||
/// </summary>
|
||||
/// <param name="request">The updated details of the brewery post.</param>
|
||||
/// <returns>A successful <see cref="BreweryServiceReturn"/> wrapping the updated brewery post.</returns>
|
||||
public async Task<BreweryServiceReturn> UpdateAsync(BreweryUpdateRequest request)
|
||||
{
|
||||
var entity = new BreweryPost
|
||||
@@ -60,6 +86,11 @@ public class BreweryService(IBreweryRepository repository) : IBreweryService
|
||||
return new BreweryServiceReturn(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a brewery post by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the brewery post to delete.</param>
|
||||
/// <returns>A task that completes once the deletion has finished.</returns>
|
||||
public Task DeleteAsync(Guid id) =>
|
||||
repository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@ using Domain.Entities;
|
||||
|
||||
namespace Service.Breweries;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data required to create a new brewery post.
|
||||
/// </summary>
|
||||
/// <param name="PostedById">The unique identifier of the user creating the post.</param>
|
||||
/// <param name="BreweryName">The name of the brewery.</param>
|
||||
/// <param name="Description">A description of the brewery.</param>
|
||||
/// <param name="Location">The location details for the new brewery post.</param>
|
||||
public record BreweryCreateRequest(
|
||||
Guid PostedById,
|
||||
string BreweryName,
|
||||
@@ -9,6 +16,14 @@ public record BreweryCreateRequest(
|
||||
BreweryLocationCreateRequest Location
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the location data required to create a new brewery post.
|
||||
/// </summary>
|
||||
/// <param name="CityId">The unique identifier of the city the brewery is located in.</param>
|
||||
/// <param name="AddressLine1">The primary address line.</param>
|
||||
/// <param name="AddressLine2">An optional secondary address line.</param>
|
||||
/// <param name="PostalCode">The postal code of the brewery's address.</param>
|
||||
/// <param name="Coordinates">Optional geographic coordinates for the brewery's location.</param>
|
||||
public record BreweryLocationCreateRequest(
|
||||
Guid CityId,
|
||||
string AddressLine1,
|
||||
@@ -17,6 +32,14 @@ public record BreweryLocationCreateRequest(
|
||||
byte[]? Coordinates
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data required to update an existing brewery post.
|
||||
/// </summary>
|
||||
/// <param name="BreweryPostId">The unique identifier of the brewery post to update.</param>
|
||||
/// <param name="PostedById">The unique identifier of the user who owns/posted the brewery.</param>
|
||||
/// <param name="BreweryName">The updated name of the brewery.</param>
|
||||
/// <param name="Description">The updated description of the brewery.</param>
|
||||
/// <param name="Location">The updated location details, or <c>null</c> to clear the location.</param>
|
||||
public record BreweryUpdateRequest(
|
||||
Guid BreweryPostId,
|
||||
Guid PostedById,
|
||||
@@ -25,6 +48,15 @@ public record BreweryUpdateRequest(
|
||||
BreweryLocationUpdateRequest? Location
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the location data required to update an existing brewery post.
|
||||
/// </summary>
|
||||
/// <param name="BreweryPostLocationId">The unique identifier of the existing brewery location record.</param>
|
||||
/// <param name="CityId">The unique identifier of the city the brewery is located in.</param>
|
||||
/// <param name="AddressLine1">The primary address line.</param>
|
||||
/// <param name="AddressLine2">An optional secondary address line.</param>
|
||||
/// <param name="PostalCode">The postal code of the brewery's address.</param>
|
||||
/// <param name="Coordinates">Optional geographic coordinates for the brewery's location.</param>
|
||||
public record BreweryLocationUpdateRequest(
|
||||
Guid BreweryPostLocationId,
|
||||
Guid CityId,
|
||||
@@ -34,18 +66,40 @@ public record BreweryLocationUpdateRequest(
|
||||
byte[]? Coordinates
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a create or update operation against a brewery post.
|
||||
/// </summary>
|
||||
public record BreweryServiceReturn
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the operation succeeded.
|
||||
/// </summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resulting brewery post when the operation succeeded; otherwise a default/unset value.
|
||||
/// </summary>
|
||||
public BreweryPost Brewery { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a message describing the failure reason, empty when <see cref="Success"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new successful result wrapping the given brewery post.
|
||||
/// </summary>
|
||||
/// <param name="brewery">The brewery post resulting from the operation.</param>
|
||||
public BreweryServiceReturn(BreweryPost brewery)
|
||||
{
|
||||
Success = true;
|
||||
Brewery = brewery;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new failed result with the given error message.
|
||||
/// </summary>
|
||||
/// <param name="message">A message describing why the operation failed.</param>
|
||||
public BreweryServiceReturn(string message)
|
||||
{
|
||||
Success = false;
|
||||
@@ -54,11 +108,44 @@ public record BreweryServiceReturn
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for retrieving, creating, updating, and deleting brewery posts.
|
||||
/// </summary>
|
||||
public interface IBreweryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a brewery post by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the brewery post.</param>
|
||||
/// <returns>The matching <see cref="BreweryPost"/>, or <c>null</c> if no such post exists.</returns>
|
||||
Task<BreweryPost?> GetByIdAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all brewery posts, optionally paginated.
|
||||
/// </summary>
|
||||
/// <param name="limit">The maximum number of results to return, or <c>null</c> for no limit.</param>
|
||||
/// <param name="offset">The number of results to skip, or <c>null</c> to start from the beginning.</param>
|
||||
/// <returns>A collection of <see cref="BreweryPost"/> entities.</returns>
|
||||
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit = null, int? offset = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new brewery post.
|
||||
/// </summary>
|
||||
/// <param name="request">The details of the brewery post to create.</param>
|
||||
/// <returns>A <see cref="BreweryServiceReturn"/> describing the outcome of the creation.</returns>
|
||||
Task<BreweryServiceReturn> CreateAsync(BreweryCreateRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing brewery post.
|
||||
/// </summary>
|
||||
/// <param name="request">The updated details of the brewery post.</param>
|
||||
/// <returns>A <see cref="BreweryServiceReturn"/> describing the outcome of the update.</returns>
|
||||
Task<BreweryServiceReturn> UpdateAsync(BreweryUpdateRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a brewery post by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the brewery post to delete.</param>
|
||||
/// <returns>A task that completes once the deletion has finished.</returns>
|
||||
Task DeleteAsync(Guid id);
|
||||
}
|
||||
|
||||
@@ -4,28 +4,63 @@ using Infrastructure.Email.Templates.Rendering;
|
||||
|
||||
namespace Service.Emails;
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for sending account-related emails, such as registration and confirmation resend emails.
|
||||
/// </summary>
|
||||
public interface IEmailService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a welcome email containing an account confirmation link to a newly registered user.
|
||||
/// </summary>
|
||||
/// <param name="createdUser">The newly created user account to email.</param>
|
||||
/// <param name="confirmationToken">The confirmation token to embed in the confirmation link.</param>
|
||||
/// <returns>A task that completes once the email has been sent.</returns>
|
||||
public Task SendRegistrationEmailAsync(
|
||||
UserAccount createdUser,
|
||||
string confirmationToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email containing a fresh account confirmation link to a user who requested a resend.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to email.</param>
|
||||
/// <param name="confirmationToken">The confirmation token to embed in the confirmation link.</param>
|
||||
/// <returns>A task that completes once the email has been sent.</returns>
|
||||
public Task SendResendConfirmationEmailAsync(
|
||||
UserAccount user,
|
||||
string confirmationToken
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IEmailService"/> that renders email templates and dispatches
|
||||
/// them via an <see cref="IEmailProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="emailProvider">Provider used to deliver the rendered emails.</param>
|
||||
/// <param name="emailTemplateProvider">Provider used to render HTML email bodies from templates.</param>
|
||||
public class EmailService(
|
||||
IEmailProvider emailProvider,
|
||||
IEmailTemplateProvider emailTemplateProvider
|
||||
) : IEmailService
|
||||
{
|
||||
/// <summary>
|
||||
/// The base URL of the website, used to build confirmation links. Read from the
|
||||
/// <c>WEBSITE_BASE_URL</c> environment variable.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown at type initialization time when the <c>WEBSITE_BASE_URL</c> environment variable is not set.
|
||||
/// </exception>
|
||||
private static readonly string WebsiteBaseUrl =
|
||||
Environment.GetEnvironmentVariable("WEBSITE_BASE_URL")
|
||||
?? throw new InvalidOperationException("WEBSITE_BASE_URL environment variable is not set");
|
||||
|
||||
/// <summary>
|
||||
/// Builds a confirmation link from the given token, renders the registration welcome email template,
|
||||
/// and sends it to the newly created user.
|
||||
/// </summary>
|
||||
/// <param name="createdUser">The newly created user account to email.</param>
|
||||
/// <param name="confirmationToken">The confirmation token to embed in the confirmation link.</param>
|
||||
/// <returns>A task that completes once the email has been sent.</returns>
|
||||
public async Task SendRegistrationEmailAsync(
|
||||
UserAccount createdUser,
|
||||
string confirmationToken
|
||||
@@ -48,6 +83,13 @@ public class EmailService(
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a confirmation link from the given token, renders the resend-confirmation email template,
|
||||
/// and sends it to the user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user account to email.</param>
|
||||
/// <param name="confirmationToken">The confirmation token to embed in the confirmation link.</param>
|
||||
/// <returns>A task that completes once the email has been sent.</returns>
|
||||
public async Task SendResendConfirmationEmailAsync(
|
||||
UserAccount user,
|
||||
string confirmationToken
|
||||
|
||||
@@ -2,13 +2,33 @@ using Domain.Entities;
|
||||
|
||||
namespace Service.UserManagement.User;
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for retrieving and updating user accounts.
|
||||
/// </summary>
|
||||
public interface IUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves all user accounts, optionally paginated.
|
||||
/// </summary>
|
||||
/// <param name="limit">The maximum number of results to return, or <c>null</c> for no limit.</param>
|
||||
/// <param name="offset">The number of results to skip, or <c>null</c> to start from the beginning.</param>
|
||||
/// <returns>A collection of <see cref="UserAccount"/> entities.</returns>
|
||||
Task<IEnumerable<UserAccount>> GetAllAsync(
|
||||
int? limit = null,
|
||||
int? offset = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a user account by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the user account.</param>
|
||||
/// <returns>The matching <see cref="UserAccount"/>.</returns>
|
||||
Task<UserAccount> GetByIdAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing user account.
|
||||
/// </summary>
|
||||
/// <param name="userAccount">The user account containing the updated data.</param>
|
||||
/// <returns>A task that completes once the update has finished.</returns>
|
||||
Task UpdateAsync(UserAccount userAccount);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,18 @@ using Infrastructure.Repository.UserAccount;
|
||||
|
||||
namespace Service.UserManagement.User;
|
||||
|
||||
/// <summary>
|
||||
/// Handles retrieval and update of user accounts.
|
||||
/// </summary>
|
||||
/// <param name="repository">Repository used to persist and query user account data.</param>
|
||||
public class UserService(IUserAccountRepository repository) : IUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves all user accounts, optionally paginated.
|
||||
/// </summary>
|
||||
/// <param name="limit">The maximum number of results to return, or <c>null</c> for no limit.</param>
|
||||
/// <param name="offset">The number of results to skip, or <c>null</c> to start from the beginning.</param>
|
||||
/// <returns>A collection of <see cref="UserAccount"/> entities.</returns>
|
||||
public async Task<IEnumerable<UserAccount>> GetAllAsync(
|
||||
int? limit = null,
|
||||
int? offset = null
|
||||
@@ -14,6 +24,12 @@ public class UserService(IUserAccountRepository repository) : IUserService
|
||||
return await repository.GetAllAsync(limit, offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a user account by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the user account.</param>
|
||||
/// <returns>The matching <see cref="UserAccount"/>.</returns>
|
||||
/// <exception cref="NotFoundException">Thrown when no user account exists with the given <paramref name="id"/>.</exception>
|
||||
public async Task<UserAccount> GetByIdAsync(Guid id)
|
||||
{
|
||||
var user = await repository.GetByIdAsync(id);
|
||||
@@ -22,6 +38,11 @@ public class UserService(IUserAccountRepository repository) : IUserService
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing user account.
|
||||
/// </summary>
|
||||
/// <param name="userAccount">The user account containing the updated data.</param>
|
||||
/// <returns>A task that completes once the update has finished.</returns>
|
||||
public async Task UpdateAsync(UserAccount userAccount)
|
||||
{
|
||||
await repository.UpdateAsync(userAccount);
|
||||
|
||||
Reference in New Issue
Block a user