diff --git a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs index bb80868..0a3c3aa 100644 --- a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserCommand.cs @@ -7,4 +7,4 @@ namespace Features.Auth.Commands.ConfirmUser; /// Validates a confirmation token and confirms the corresponding user account. /// /// The confirmation token issued to the user, typically delivered via email. -public record ConfirmUserCommand(string Token) : IRequest; \ No newline at end of file +public record ConfirmUserCommand(string Token) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs index 979698d..230d799 100644 --- a/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs @@ -13,22 +13,26 @@ namespace Features.Auth.Commands.ConfirmUser; /// /// Repository used to look up and confirm user accounts. /// Service used to validate the confirmation token. -public class ConfirmUserHandler( - IAuthRepository authRepository, - ITokenService tokenService -) : IRequestHandler +public class ConfirmUserHandler(IAuthRepository authRepository, ITokenService tokenService) + : IRequestHandler { /// /// Thrown when the confirmation token is invalid or expired, or when the associated user account cannot be found. /// - public async Task Handle(ConfirmUserCommand request, CancellationToken cancellationToken) + public async Task Handle( + ConfirmUserCommand request, + CancellationToken cancellationToken + ) { - ValidatedToken validatedToken = await tokenService.ValidateConfirmationTokenAsync(request.Token); + ValidatedToken validatedToken = await tokenService.ValidateConfirmationTokenAsync( + request.Token + ); UserAccount? user = await authRepository.ConfirmUserAccountAsync(validatedToken.UserId); - if (user == null) throw new UnauthorizedException("User account not found"); + if (user == null) + throw new UnauthorizedException("User account not found"); return new ConfirmationPayload(user.UserAccountId, DateTime.UtcNow); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs index ed8709e..d065235 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenCommand.cs @@ -7,4 +7,4 @@ namespace Features.Auth.Commands.RefreshToken; /// Exchanges a valid refresh token for a new access/refresh token pair. Bound directly from the /// request body of POST /api/auth/refresh. /// -public record RefreshTokenCommand(string RefreshToken) : IRequest; \ No newline at end of file +public record RefreshTokenCommand(string RefreshToken) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs index 977e739..91c9d49 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs @@ -12,10 +12,17 @@ namespace Features.Auth.Commands.RefreshToken; public class RefreshTokenHandler(ITokenService tokenService) : IRequestHandler { - public async Task Handle(RefreshTokenCommand request, CancellationToken cancellationToken) + public async Task Handle( + RefreshTokenCommand request, + CancellationToken cancellationToken + ) { RefreshTokenResult result = await tokenService.RefreshTokenAsync(request.RefreshToken); - return new LoginPayload(result.UserAccount.UserAccountId, result.UserAccount.Username, result.RefreshToken, - result.AccessToken); + return new LoginPayload( + result.UserAccount.UserAccountId, + result.UserAccount.Username, + result.RefreshToken, + result.AccessToken + ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs index 24070f5..b6f2a2e 100644 --- a/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs +++ b/web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenValidator.cs @@ -12,8 +12,6 @@ public class RefreshTokenValidator : AbstractValidator /// public RefreshTokenValidator() { - RuleFor(x => x.RefreshToken) - .NotEmpty() - .WithMessage("Refresh token is required"); + RuleFor(x => x.RefreshToken).NotEmpty().WithMessage("Refresh token is required"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs index ab2965a..c6d7d05 100644 --- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserCommand.cs @@ -25,4 +25,4 @@ public record RegisterUserCommand( string Email, DateTime DateOfBirth, string Password -) : IRequest; \ No newline at end of file +) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs index 39915bd..0adb299 100644 --- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs @@ -28,7 +28,10 @@ public class RegisterUserHandler( /// /// Thrown when an existing account already has the same username or email address. /// - public async Task Handle(RegisterUserCommand request, CancellationToken cancellationToken) + public async Task Handle( + RegisterUserCommand request, + CancellationToken cancellationToken + ) { await ValidateUserDoesNotExist(request.Username, request.Email); @@ -48,14 +51,23 @@ public class RegisterUserHandler( string confirmationToken = tokenService.GenerateConfirmationToken(createdUser); if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) - return new RegistrationPayload(createdUser.UserAccountId, createdUser.Username, string.Empty, string.Empty, - false); + return new RegistrationPayload( + createdUser.UserAccountId, + createdUser.Username, + string.Empty, + string.Empty, + false + ); bool emailSent = false; try { await mediator.Send( - new SendRegistrationEmailCommand(createdUser.FirstName, createdUser.Email, confirmationToken), + new SendRegistrationEmailCommand( + createdUser.FirstName, + createdUser.Email, + confirmationToken + ), cancellationToken ); emailSent = true; @@ -67,8 +79,13 @@ public class RegisterUserHandler( // ignored } - return new RegistrationPayload(createdUser.UserAccountId, createdUser.Username, refreshToken, accessToken, - emailSent); + return new RegistrationPayload( + createdUser.UserAccountId, + createdUser.Username, + refreshToken, + accessToken, + emailSent + ); } /// @@ -82,4 +99,4 @@ public class RegisterUserHandler( if (existingUsername != null || existingEmail != null) throw new ConflictException("Username or email already exists"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs index bca2aaa..04125df 100644 --- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs +++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserValidator.cs @@ -61,8 +61,6 @@ public class RegisterUserValidator : AbstractValidator .Matches("[0-9]") .WithMessage("Password must contain at least one number") .Matches("[^a-zA-Z0-9]") - .WithMessage( - "Password must contain at least one special character" - ); + .WithMessage("Password must contain at least one special character"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs index 66c4fbf..5b276fc 100644 --- a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs +++ b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailCommand.cs @@ -6,4 +6,4 @@ namespace Features.Auth.Commands.ResendConfirmationEmail; /// Resends the account confirmation email to a user, generating a fresh confirmation token. /// /// The unique identifier of the user requesting the resend. -public record ResendConfirmationEmailCommand(Guid UserId) : IRequest; \ No newline at end of file +public record ResendConfirmationEmailCommand(Guid UserId) : IRequest; diff --git a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs index 5049260..33eb3f9 100644 --- a/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs +++ b/web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs @@ -23,12 +23,17 @@ public class ResendConfirmationEmailHandler( IMediator mediator ) : IRequestHandler { - public async Task Handle(ResendConfirmationEmailCommand request, CancellationToken cancellationToken) + public async Task Handle( + ResendConfirmationEmailCommand request, + CancellationToken cancellationToken + ) { UserAccount? user = await authRepository.GetUserByIdAsync(request.UserId); - if (user == null) return; // Silent return to prevent user enumeration + if (user == null) + return; // Silent return to prevent user enumeration - if (await authRepository.IsUserVerifiedAsync(request.UserId)) return; // Already confirmed, no-op + if (await authRepository.IsUserVerifiedAsync(request.UserId)) + return; // Already confirmed, no-op string confirmationToken = tokenService.GenerateConfirmationToken(user); await mediator.Send( @@ -36,4 +41,4 @@ public class ResendConfirmationEmailHandler( cancellationToken ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Controllers/AuthController.cs b/web/backend/Features/Features.Auth/Controllers/AuthController.cs index 12fe44a..308c36d 100644 --- a/web/backend/Features/Features.Auth/Controllers/AuthController.cs +++ b/web/backend/Features/Features.Auth/Controllers/AuthController.cs @@ -40,11 +40,14 @@ public class AuthController(IMediator mediator) : ControllerBase ) { RegistrationPayload payload = await mediator.Send(command); - return Created("/", new ResponseBody - { - Message = "User registered successfully.", - Payload = payload - }); + return Created( + "/", + new ResponseBody + { + Message = "User registered successfully.", + Payload = payload, + } + ); } /// @@ -61,11 +64,13 @@ public class AuthController(IMediator mediator) : ControllerBase public async Task>> Login([FromBody] LoginQuery query) { LoginPayload payload = await mediator.Send(query); - return Ok(new ResponseBody - { - Message = "Logged in successfully.", - Payload = payload - }); + return Ok( + new ResponseBody + { + Message = "Logged in successfully.", + Payload = payload, + } + ); } /// @@ -78,14 +83,18 @@ public class AuthController(IMediator mediator) : ControllerBase /// The confirmation token supplied via the confirmation email link. /// A 200 OK result wrapping a of . [HttpPost("confirm")] - public async Task>> Confirm([FromQuery] string token) + public async Task>> Confirm( + [FromQuery] string token + ) { ConfirmationPayload payload = await mediator.Send(new ConfirmUserCommand(token)); - return Ok(new ResponseBody - { - Message = "User with ID " + payload.UserAccountId + " is confirmed.", - Payload = payload - }); + return Ok( + new ResponseBody + { + Message = "User with ID " + payload.UserAccountId + " is confirmed.", + Payload = payload, + } + ); } /// @@ -119,10 +128,12 @@ public class AuthController(IMediator mediator) : ControllerBase ) { LoginPayload payload = await mediator.Send(command); - return Ok(new ResponseBody - { - Message = "Token refreshed successfully.", - Payload = payload - }); + return Ok( + new ResponseBody + { + Message = "Token refreshed successfully.", + Payload = payload, + } + ); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs b/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs index 16c844e..7e47573 100644 --- a/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs +++ b/web/backend/Features/Features.Auth/DependencyInjection/FeaturesAuthServiceCollectionExtensions.cs @@ -17,4 +17,4 @@ public static class FeaturesAuthServiceCollectionExtensions services.AddScoped(); return services; } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs b/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs index 0b1bab4..64e5a85 100644 --- a/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs +++ b/web/backend/Features/Features.Auth/Dtos/AuthPayloads.cs @@ -35,4 +35,4 @@ public record RegistrationPayload( /// /// The unique identifier of the user account that was confirmed. /// The date and time at which the account was confirmed. -public record ConfirmationPayload(Guid UserAccountId, DateTime ConfirmedDate); \ No newline at end of file +public record ConfirmationPayload(Guid UserAccountId, DateTime ConfirmedDate); diff --git a/web/backend/Features/Features.Auth/Features.Auth.csproj b/web/backend/Features/Features.Auth/Features.Auth.csproj index 7adcdc9..3710b91 100644 --- a/web/backend/Features/Features.Auth/Features.Auth.csproj +++ b/web/backend/Features/Features.Auth/Features.Auth.csproj @@ -7,16 +7,16 @@ - + - - - - - - - + + + + + + + diff --git a/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs b/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs index 6306d97..d3ee64d 100644 --- a/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs +++ b/web/backend/Features/Features.Auth/Queries/Login/LoginHandler.cs @@ -46,4 +46,4 @@ public class LoginHandler( return new LoginPayload(user.UserAccountId, user.Username, refreshToken, accessToken); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs b/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs index 3ae72cb..081e6aa 100644 --- a/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs +++ b/web/backend/Features/Features.Auth/Queries/Login/LoginQuery.cs @@ -7,4 +7,4 @@ namespace Features.Auth.Queries.Login; /// Authenticates a user using their username and password and issues new tokens. Bound directly /// from the request body of POST /api/auth/login. /// -public record LoginQuery(string Username, string Password) : IRequest; \ No newline at end of file +public record LoginQuery(string Username, string Password) : IRequest; diff --git a/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs b/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs index 6b60c07..8458414 100644 --- a/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs +++ b/web/backend/Features/Features.Auth/Queries/Login/LoginValidator.cs @@ -17,4 +17,4 @@ public class LoginValidator : AbstractValidator RuleFor(x => x.Password).NotEmpty().WithMessage("Password is required"); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs index 7f4eb6a..03fe138 100644 --- a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs +++ b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs @@ -81,8 +81,8 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) } } - return await GetUserByIdAsync(userAccountId) ?? - throw new Exception("Failed to retrieve newly registered user."); + return await GetUserByIdAsync(userAccountId) + ?? throw new Exception("Failed to retrieve newly registered user."); } /// @@ -92,9 +92,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// Email address to search for /// UserAccount if found, null otherwise /// Thrown when the database command fails. - public async Task GetUserByEmailAsync( - string email - ) + public async Task GetUserByEmailAsync(string email) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -114,9 +112,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// Username to search for /// UserAccount if found, null otherwise /// Thrown when the database command fails. - public async Task GetUserByUsernameAsync( - string username - ) + public async Task GetUserByUsernameAsync(string username) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -136,9 +132,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// ID of the user account /// Active UserCredential if found, null otherwise /// Thrown when the database command fails. - public async Task GetActiveCredentialByUserAccountIdAsync( - Guid userAccountId - ) + public async Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -158,10 +152,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// ID of the user account /// New hashed password /// Thrown when the database command fails. - public async Task RotateCredentialAsync( - Guid userAccountId, - string newPasswordHash - ) + public async Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -180,9 +171,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// ID of the user account /// UserAccount if found, null otherwise /// Thrown when the database command fails. - public async Task GetUserByIdAsync( - Guid userAccountId - ) + public async Task GetUserByIdAsync(Guid userAccountId) { await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -207,15 +196,15 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// Thrown when the database command fails for a reason other than /// a duplicate verification record. /// - public async Task ConfirmUserAccountAsync( - Guid userAccountId - ) + public async Task ConfirmUserAccountAsync(Guid userAccountId) { UserAccount? user = await GetUserByIdAsync(userAccountId); - if (user == null) return null; + if (user == null) + return null; // Idempotency: if already verified, treat as successful confirmation. - if (await IsUserVerifiedAsync(userAccountId)) return user; + if (await IsUserVerifiedAsync(userAccountId)) + return user; await using DbConnection connection = await CreateConnection(); await using DbCommand command = connection.CreateCommand(); @@ -270,15 +259,12 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) return ex.Number == 2601 || ex.Number == 2627; } - /// /// Maps a data reader row to a UserAccount entity. /// /// The data reader positioned on the row to map. /// The mapped instance. - protected override UserAccount MapToEntity( - DbDataReader reader - ) + protected override UserAccount MapToEntity(DbDataReader reader) { return new UserAccount { @@ -292,9 +278,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) ? null : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), - Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) - ? null - : (byte[])reader["Timer"] + Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null : (byte[])reader["Timer"], }; } @@ -308,12 +292,10 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) { UserCredential entity = new() { - UserCredentialId = reader.GetGuid( - reader.GetOrdinal("UserCredentialId") - ), + UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")), UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), Hash = reader.GetString(reader.GetOrdinal("Hash")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")) + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), }; // Optional columns @@ -327,7 +309,8 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) "Timer", StringComparison.OrdinalIgnoreCase ) - ) ?? false; + ) + ?? false; if (hasTimer) entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) @@ -344,15 +327,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory) /// The command to add the parameter to. /// The parameter name (including any prefix, e.g. "@Username"). /// The parameter value, or null to bind . - private static void AddParameter( - DbCommand command, - string name, - object? value - ) + private static void AddParameter(DbCommand command, string name, object? value) { DbParameter p = command.CreateParameter(); p.ParameterName = name; p.Value = value ?? DBNull.Value; command.Parameters.Add(p); } -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs index 5c791b7..d405891 100644 --- a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs +++ b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs @@ -49,9 +49,7 @@ public interface IAuthRepository /// /// ID of the user account /// Active UserCredential if found, null otherwise - Task GetActiveCredentialByUserAccountIdAsync( - Guid userAccountId - ); + Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId); /// /// Rotates a user's credential by invalidating all existing credentials and creating a new one. @@ -81,4 +79,4 @@ public interface IAuthRepository /// ID of the user account /// True if the user has a verification record, false otherwise Task IsUserVerifiedAsync(Guid userAccountId); -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Services/ITokenService.cs b/web/backend/Features/Features.Auth/Services/ITokenService.cs index 09b34ab..4285367 100644 --- a/web/backend/Features/Features.Auth/Services/ITokenService.cs +++ b/web/backend/Features/Features.Auth/Services/ITokenService.cs @@ -15,7 +15,7 @@ public enum TokenType RefreshToken, /// A short-lived token used to confirm a user's email/account. - ConfirmationToken + ConfirmationToken, } /// @@ -32,11 +32,7 @@ public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Princ /// The user account associated with the refreshed session. /// The newly issued refresh token. /// The newly issued access token. -public record RefreshTokenResult( - UserAccount UserAccount, - string RefreshToken, - string AccessToken -); +public record RefreshTokenResult(UserAccount UserAccount, string RefreshToken, string AccessToken); /// /// Defines the expiration windows, in hours, for each type of token issued by . @@ -85,7 +81,8 @@ public interface ITokenService /// The enum type identifying which kind of token to generate. Must be . /// The user account to generate the token for. /// The signed token string corresponding to the requested token type. - string GenerateToken(UserAccount user) where T : struct, Enum; + string GenerateToken(UserAccount user) + where T : struct, Enum; /// /// Validates an access token. @@ -114,4 +111,4 @@ public interface ITokenService /// The refresh token string to validate and exchange. /// A containing the user and newly issued tokens. Task RefreshTokenAsync(string refreshTokenString); -} \ No newline at end of file +} diff --git a/web/backend/Features/Features.Auth/Services/TokenService.cs b/web/backend/Features/Features.Auth/Services/TokenService.cs index e9b4a34..1bbf13f 100644 --- a/web/backend/Features/Features.Auth/Services/TokenService.cs +++ b/web/backend/Features/Features.Auth/Services/TokenService.cs @@ -29,25 +29,28 @@ public class TokenService : ITokenService /// Thrown when any of the ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, or /// CONFIRMATION_TOKEN_SECRET environment variables are not set. /// - public TokenService( - ITokenInfrastructure tokenInfrastructure, - IAuthRepository authRepository - ) + public TokenService(ITokenInfrastructure tokenInfrastructure, IAuthRepository authRepository) { _tokenInfrastructure = tokenInfrastructure; _authRepository = authRepository; - _accessTokenSecret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "ACCESS_TOKEN_SECRET environment variable is not set"); + _accessTokenSecret = + Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET") + ?? throw new InvalidOperationException( + "ACCESS_TOKEN_SECRET environment variable is not set" + ); - _refreshTokenSecret = Environment.GetEnvironmentVariable("REFRESH_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "REFRESH_TOKEN_SECRET environment variable is not set"); + _refreshTokenSecret = + Environment.GetEnvironmentVariable("REFRESH_TOKEN_SECRET") + ?? throw new InvalidOperationException( + "REFRESH_TOKEN_SECRET environment variable is not set" + ); - _confirmationTokenSecret = Environment.GetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "CONFIRMATION_TOKEN_SECRET environment variable is not set"); + _confirmationTokenSecret = + Environment.GetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET") + ?? throw new InvalidOperationException( + "CONFIRMATION_TOKEN_SECRET environment variable is not set" + ); } /// @@ -59,7 +62,12 @@ public class TokenService : ITokenService public string GenerateAccessToken(UserAccount user) { DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours); - return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _accessTokenSecret); + return _tokenInfrastructure.GenerateJwt( + user.UserAccountId, + user.Username, + expiresAt, + _accessTokenSecret + ); } /// @@ -70,8 +78,15 @@ public class TokenService : ITokenService /// The signed refresh token string. public string GenerateRefreshToken(UserAccount user) { - DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.RefreshTokenHours); - return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _refreshTokenSecret); + DateTime expiresAt = DateTime.UtcNow.AddHours( + TokenServiceExpirationHours.RefreshTokenHours + ); + return _tokenInfrastructure.GenerateJwt( + user.UserAccountId, + user.Username, + expiresAt, + _refreshTokenSecret + ); } /// @@ -82,8 +97,15 @@ public class TokenService : ITokenService /// The signed confirmation token string. public string GenerateConfirmationToken(UserAccount user) { - DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.ConfirmationTokenHours); - return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _confirmationTokenSecret); + DateTime expiresAt = DateTime.UtcNow.AddHours( + TokenServiceExpirationHours.ConfirmationTokenHours + ); + return _tokenInfrastructure.GenerateJwt( + user.UserAccountId, + user.Username, + expiresAt, + _confirmationTokenSecret + ); } /// @@ -97,7 +119,8 @@ public class TokenService : ITokenService /// /// Thrown when is not or does not resolve to a known token type. /// - public string GenerateToken(UserAccount user) where T : struct, Enum + public string GenerateToken(UserAccount user) + where T : struct, Enum { if (typeof(T) != typeof(TokenType)) throw new InvalidOperationException("Invalid token type"); @@ -112,7 +135,7 @@ public class TokenService : ITokenService TokenType.AccessToken => GenerateAccessToken(user), TokenType.RefreshToken => GenerateRefreshToken(user), TokenType.ConfirmationToken => GenerateConfirmationToken(user), - _ => throw new InvalidOperationException("Invalid token type") + _ => throw new InvalidOperationException("Invalid token type"), }; } @@ -189,7 +212,11 @@ public class TokenService : ITokenService /// Thrown when required claims are missing, the user ID claim is not a valid , /// or the underlying token validation fails for any other reason (e.g. expired or invalid signature). /// - private async Task ValidateTokenInternalAsync(string token, string secret, string tokenType) + private async Task ValidateTokenInternalAsync( + string token, + string secret, + string tokenType + ) { try { @@ -199,7 +226,9 @@ public class TokenService : ITokenService string? usernameClaim = principal.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value; if (string.IsNullOrEmpty(userIdClaim) || string.IsNullOrEmpty(usernameClaim)) - throw new UnauthorizedException($"Invalid {tokenType} token: missing required claims"); + throw new UnauthorizedException( + $"Invalid {tokenType} token: missing required claims" + ); if (!Guid.TryParse(userIdClaim, out Guid userId)) throw new UnauthorizedException($"Invalid {tokenType} token: malformed user ID"); @@ -215,4 +244,4 @@ public class TokenService : ITokenService throw new UnauthorizedException($"Failed to validate {tokenType} token: {e.Message}"); } } -} \ No newline at end of file +}