diff --git a/src/Core/API/API.Core/API.Core.csproj b/src/Core/API/API.Core/API.Core.csproj index 938fed6..5acab8a 100644 --- a/src/Core/API/API.Core/API.Core.csproj +++ b/src/Core/API/API.Core/API.Core.csproj @@ -20,12 +20,9 @@ - - - + + + diff --git a/src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs b/src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs index 0d08dee..5b9b19d 100644 --- a/src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs +++ b/src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs @@ -1,4 +1,11 @@ +using Domain.Entities; +using Org.BouncyCastle.Asn1.Cms; + namespace API.Core.Contracts.Auth; -public record UserDTO(Guid UserAccountId, string Username); -public record AuthPayload(UserDTO User, string AccessToken, DateTime CreatedAt, DateTime ExpiresAt); +public record AuthPayload( + Guid UserAccountId, + string Username, + string RefreshToken, + string AccessToken +); diff --git a/src/Core/API/API.Core/Contracts/Auth/Login.cs b/src/Core/API/API.Core/Contracts/Auth/Login.cs index 96a8126..c0bb5f8 100644 --- a/src/Core/API/API.Core/Contracts/Auth/Login.cs +++ b/src/Core/API/API.Core/Contracts/Auth/Login.cs @@ -5,19 +5,16 @@ namespace API.Core.Contracts.Auth; public record LoginRequest { - public string Username { get; init; } = default!; - public string Password { get; init; } = default!; + public string Username { get; init; } = default!; + public string Password { get; init; } = default!; } public class LoginRequestValidator : AbstractValidator { - public LoginRequestValidator() - { - RuleFor(x => x.Username) - .NotEmpty().WithMessage("Username is required"); + public LoginRequestValidator() + { + RuleFor(x => x.Username).NotEmpty().WithMessage("Username is required"); - RuleFor(x => x.Password) - .NotEmpty().WithMessage("Password is required"); - } + RuleFor(x => x.Password).NotEmpty().WithMessage("Password is required"); + } } - diff --git a/src/Core/API/API.Core/Contracts/Auth/Register.cs b/src/Core/API/API.Core/Contracts/Auth/Register.cs index 012575d..31c3541 100644 --- a/src/Core/API/API.Core/Contracts/Auth/Register.cs +++ b/src/Core/API/API.Core/Contracts/Auth/Register.cs @@ -17,38 +17,55 @@ public class RegisterRequestValidator : AbstractValidator public RegisterRequestValidator() { RuleFor(x => x.Username) - .NotEmpty().WithMessage("Username is required") - .Length(3, 64).WithMessage("Username must be between 3 and 64 characters") + .NotEmpty() + .WithMessage("Username is required") + .Length(3, 64) + .WithMessage("Username must be between 3 and 64 characters") .Matches("^[a-zA-Z0-9._-]+$") - .WithMessage("Username can only contain letters, numbers, dots, underscores, and hyphens"); + .WithMessage( + "Username can only contain letters, numbers, dots, underscores, and hyphens" + ); RuleFor(x => x.FirstName) - .NotEmpty().WithMessage("First name is required") - .MaximumLength(128).WithMessage("First name cannot exceed 128 characters"); + .NotEmpty() + .WithMessage("First name is required") + .MaximumLength(128) + .WithMessage("First name cannot exceed 128 characters"); RuleFor(x => x.LastName) - .NotEmpty().WithMessage("Last name is required") - .MaximumLength(128).WithMessage("Last name cannot exceed 128 characters"); + .NotEmpty() + .WithMessage("Last name is required") + .MaximumLength(128) + .WithMessage("Last name cannot exceed 128 characters"); RuleFor(x => x.Email) - .NotEmpty().WithMessage("Email is required") - .EmailAddress().WithMessage("Invalid email format") - .MaximumLength(128).WithMessage("Email cannot exceed 128 characters"); + .NotEmpty() + .WithMessage("Email is required") + .EmailAddress() + .WithMessage("Invalid email format") + .MaximumLength(128) + .WithMessage("Email cannot exceed 128 characters"); RuleFor(x => x.DateOfBirth) - .NotEmpty().WithMessage("Date of birth is required") + .NotEmpty() + .WithMessage("Date of birth is required") .LessThan(DateTime.Today.AddYears(-19)) .WithMessage("You must be at least 19 years old to register"); RuleFor(x => x.Password) - .NotEmpty().WithMessage("Password is required") - .MinimumLength(8).WithMessage("Password must be at least 8 characters") - .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter") - .Matches("[a-z]").WithMessage("Password must contain at least one lowercase letter") - .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"); + .NotEmpty() + .WithMessage("Password is required") + .MinimumLength(8) + .WithMessage("Password must be at least 8 characters") + .Matches("[A-Z]") + .WithMessage("Password must contain at least one uppercase letter") + .Matches("[a-z]") + .WithMessage("Password must contain at least one lowercase letter") + .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" + ); } } - - - diff --git a/src/Core/API/API.Core/Contracts/Common/ResponseBody.cs b/src/Core/API/API.Core/Contracts/Common/ResponseBody.cs index e5a6e71..12acd29 100644 --- a/src/Core/API/API.Core/Contracts/Common/ResponseBody.cs +++ b/src/Core/API/API.Core/Contracts/Common/ResponseBody.cs @@ -2,11 +2,11 @@ namespace API.Core.Contracts.Common; public record ResponseBody { - public required string Message { get; init; } - public required T Payload { get; init; } + public required string Message { get; init; } + public required T Payload { get; init; } } public record ResponseBody { - public required string Message { get; init; } + public required string Message { get; init; } } diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs index 7243141..2715896 100644 --- a/src/Core/API/API.Core/Controllers/AuthController.cs +++ b/src/Core/API/API.Core/Controllers/AuthController.cs @@ -1,7 +1,6 @@ using API.Core.Contracts.Auth; using API.Core.Contracts.Common; using Domain.Entities; -using Infrastructure.Jwt; using Microsoft.AspNetCore.Mvc; using Service.Auth; @@ -9,33 +8,36 @@ namespace API.Core.Controllers { [ApiController] [Route("api/[controller]")] - public class AuthController(IRegisterService register, ILoginService login, ITokenInfrastructure tokenInfrastructure) : ControllerBase + public class AuthController(IRegisterService register, ILoginService login) + : ControllerBase { [HttpPost("register")] - public async Task> Register([FromBody] RegisterRequest req) + public async Task> Register( + [FromBody] RegisterRequest req + ) { - var created = await register.RegisterAsync(new UserAccount - { - UserAccountId = Guid.Empty, - Username = req.Username, - FirstName = req.FirstName, - LastName = req.LastName, - Email = req.Email, - DateOfBirth = req.DateOfBirth - }, req.Password); - - var jwtExpiresAt = DateTime.UtcNow.AddHours(1); - var jwt = tokenInfrastructure.GenerateJwt(created.UserAccountId, created.Username, jwtExpiresAt + AuthServiceReturn rtn = await register.RegisterAsync( + new UserAccount + { + UserAccountId = Guid.Empty, + Username = req.Username, + FirstName = req.FirstName, + LastName = req.LastName, + Email = req.Email, + DateOfBirth = req.DateOfBirth, + }, + req.Password ); var response = new ResponseBody { Message = "User registered successfully.", Payload = new AuthPayload( - new UserDTO(created.UserAccountId, created.Username), - jwt, - DateTime.UtcNow, - jwtExpiresAt) + rtn.UserAccount.UserAccountId, + rtn.UserAccount.Username, + rtn.RefreshToken, + rtn.AccessToken + ), }; return Created("/", response); } @@ -43,18 +45,20 @@ namespace API.Core.Controllers [HttpPost("login")] public async Task Login([FromBody] LoginRequest req) { - var userAccount = await login.LoginAsync(req.Username, req.Password); + var rtn = await login.LoginAsync(req.Username, req.Password); - UserDTO dto = new(userAccount.UserAccountId, userAccount.Username); - - var jwtExpiresAt = DateTime.UtcNow.AddHours(1); - var jwt = tokenInfrastructure.GenerateJwt(userAccount.UserAccountId, userAccount.Username, jwtExpiresAt); - - return Ok(new ResponseBody - { - Message = "Logged in successfully.", - Payload = new AuthPayload(dto, jwt, DateTime.UtcNow, jwtExpiresAt) - }); + return Ok( + new ResponseBody + { + Message = "Logged in successfully.", + Payload = new AuthPayload( + rtn.UserAccount.UserAccountId, + rtn.UserAccount.Username, + rtn.RefreshToken, + rtn.AccessToken + ), + } + ); } } } diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs index ff70e6d..604ba4d 100644 --- a/src/Core/API/API.Core/Program.cs +++ b/src/Core/API/API.Core/Program.cs @@ -1,7 +1,11 @@ using API.Core; +using API.Core.Contracts.Common; using Domain.Exceptions; using FluentValidation; using FluentValidation.AspNetCore; +using Infrastructure.Email; +using Infrastructure.Email.Templates; +using Infrastructure.Email.Templates.Rendering; using Infrastructure.Jwt; using Infrastructure.PasswordHashing; using Infrastructure.Repository.Auth; @@ -9,12 +13,8 @@ using Infrastructure.Repository.Sql; using Infrastructure.Repository.UserAccount; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Service.UserManagement.User; -using API.Core.Contracts.Common; -using Infrastructure.Email; -using Infrastructure.Email.Templates; -using Infrastructure.Email.Templates.Rendering; using Service.Auth; +using Service.UserManagement.User; var builder = WebApplication.CreateBuilder(args); @@ -45,7 +45,10 @@ if (!builder.Environment.IsProduction()) // Configure Dependency Injection ------------------------------------------------------------------------------------- -builder.Services.AddSingleton(); +builder.Services.AddSingleton< + ISqlConnectionFactory, + DefaultSqlConnectionFactory +>(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -53,6 +56,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/Core/Service/Service.Auth.Tests/LoginService.test.cs b/src/Core/Service/Service.Auth.Tests/LoginService.test.cs index b266d1e..12f2e20 100644 --- a/src/Core/Service/Service.Auth.Tests/LoginService.test.cs +++ b/src/Core/Service/Service.Auth.Tests/LoginService.test.cs @@ -11,15 +11,18 @@ public class LoginServiceTest { private readonly Mock _authRepoMock; private readonly Mock _passwordInfraMock; + private readonly Mock _tokenServiceMock; private readonly LoginService _loginService; public LoginServiceTest() { _authRepoMock = new Mock(); _passwordInfraMock = new Mock(); + _tokenServiceMock = new Mock(); _loginService = new LoginService( _authRepoMock.Object, - _passwordInfraMock.Object + _passwordInfraMock.Object, + _tokenServiceMock.Object ); } @@ -63,13 +66,26 @@ public class LoginServiceTest .Setup(x => x.Verify(It.IsAny(), It.IsAny())) .Returns(true); + _tokenServiceMock + .Setup(x => x.GenerateAccessToken(It.IsAny())) + .Returns("access-token"); + + _tokenServiceMock + .Setup(x => x.GenerateRefreshToken(It.IsAny())) + .Returns("refresh-token"); + // Act - var result = await _loginService.LoginAsync(username, It.IsAny()); + var result = await _loginService.LoginAsync( + username, + It.IsAny() + ); // Assert result.Should().NotBeNull(); - result.UserAccountId.Should().Be(userAccountId); - result.Username.Should().Be(username); + result.UserAccount.UserAccountId.Should().Be(userAccountId); + result.UserAccount.Username.Should().Be(username); + result.AccessToken.Should().Be("access-token"); + result.RefreshToken.Should().Be("refresh-token"); _authRepoMock.Verify( x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId), diff --git a/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs b/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs index 2b909bc..b5d2675 100644 --- a/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs +++ b/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs @@ -1,8 +1,6 @@ using Domain.Entities; using Domain.Exceptions; using FluentAssertions; -using Infrastructure.Email; -using Infrastructure.Email.Templates.Rendering; using Infrastructure.PasswordHashing; using Infrastructure.Repository.Auth; using Moq; @@ -13,27 +11,24 @@ public class RegisterServiceTest { private readonly Mock _authRepoMock; private readonly Mock _passwordInfraMock; - private readonly Mock _emailProviderMock; - private readonly Mock _emailTemplateProviderMock; + private readonly Mock _tokenServiceMock; private readonly RegisterService _registerService; public RegisterServiceTest() { _authRepoMock = new Mock(); _passwordInfraMock = new Mock(); - _emailProviderMock = new Mock(); - _emailTemplateProviderMock = new Mock(); + _tokenServiceMock = new Mock(); _registerService = new RegisterService( _authRepoMock.Object, _passwordInfraMock.Object, - _emailProviderMock.Object, - _emailTemplateProviderMock.Object + _tokenServiceMock.Object ); } [Fact] - public async Task RegisterAsync_WithValidData_CreatesUserAndSendsEmail() + public async Task RegisterAsync_WithValidData_CreatesUserAndReturnsAuthServiceReturn() { // Arrange var userAccount = new UserAccount @@ -48,7 +43,6 @@ public class RegisterServiceTest const string password = "SecurePassword123!"; const string hashedPassword = "hashed_password_value"; var expectedUserId = Guid.NewGuid(); - const string expectedEmailHtml = "Welcome!"; // Mock: No existing user _authRepoMock @@ -89,36 +83,28 @@ public class RegisterServiceTest } ); - // Mock: Email template rendering - _emailTemplateProviderMock - .Setup(x => - x.RenderUserRegisteredEmailAsync( - userAccount.FirstName, - It.IsAny() - ) - ) - .ReturnsAsync(expectedEmailHtml); + // Mock: Token generation + _tokenServiceMock + .Setup(x => x.GenerateAccessToken(It.IsAny())) + .Returns("access-token"); - // Mock: Email sending - _emailProviderMock - .Setup(x => - x.SendAsync( - userAccount.Email, - "Welcome to The Biergarten App!", - expectedEmailHtml, - true - ) - ) - .Returns(Task.CompletedTask); + _tokenServiceMock + .Setup(x => x.GenerateRefreshToken(It.IsAny())) + .Returns("refresh-token"); // Act - var result = await _registerService.RegisterAsync(userAccount, password); + var result = await _registerService.RegisterAsync( + userAccount, + password + ); // Assert result.Should().NotBeNull(); - result.UserAccountId.Should().Be(expectedUserId); - result.Username.Should().Be(userAccount.Username); - result.Email.Should().Be(userAccount.Email); + result.UserAccount.UserAccountId.Should().Be(expectedUserId); + result.UserAccount.Username.Should().Be(userAccount.Username); + result.UserAccount.Email.Should().Be(userAccount.Email); + result.AccessToken.Should().Be("access-token"); + result.RefreshToken.Should().Be("refresh-token"); // Verify all mocks were called as expected _authRepoMock.Verify( @@ -142,24 +128,6 @@ public class RegisterServiceTest ), Times.Once ); - _emailTemplateProviderMock.Verify( - x => - x.RenderUserRegisteredEmailAsync( - userAccount.FirstName, - It.IsAny() - ), - Times.Once - ); - _emailProviderMock.Verify( - x => - x.SendAsync( - userAccount.Email, - "Welcome to The Biergarten App!", - expectedEmailHtml, - true - ), - Times.Once - ); } [Fact] @@ -195,7 +163,8 @@ public class RegisterServiceTest .ReturnsAsync((UserAccount?)null); // Act - var act = async () => await _registerService.RegisterAsync(userAccount, password); + var act = async () => + await _registerService.RegisterAsync(userAccount, password); // Assert await act.Should() @@ -215,18 +184,6 @@ public class RegisterServiceTest ), Times.Never ); - - // Verify email was never sent - _emailProviderMock.Verify( - x => - x.SendAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny() - ), - Times.Never - ); } [Fact] @@ -262,7 +219,8 @@ public class RegisterServiceTest .ReturnsAsync(existingUser); // Act - var act = async () => await _registerService.RegisterAsync(userAccount, password); + var act = async () => + await _registerService.RegisterAsync(userAccount, password); // Assert await act.Should() @@ -323,14 +281,13 @@ public class RegisterServiceTest ) .ReturnsAsync(new UserAccount { UserAccountId = Guid.NewGuid() }); - _emailTemplateProviderMock - .Setup(x => - x.RenderUserRegisteredEmailAsync( - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync(""); + _tokenServiceMock + .Setup(x => x.GenerateAccessToken(It.IsAny())) + .Returns("access-token"); + + _tokenServiceMock + .Setup(x => x.GenerateRefreshToken(It.IsAny())) + .Returns("refresh-token"); // Act await _registerService.RegisterAsync(userAccount, plainPassword); @@ -350,152 +307,4 @@ public class RegisterServiceTest Times.Once ); } - - [Fact] - public async Task RegisterAsync_EmailConfirmationLink_ContainsUserEmail() - { - // Arrange - var userAccount = new UserAccount - { - Username = "testuser", - FirstName = "Test", - LastName = "User", - Email = "test@example.com", - DateOfBirth = new DateTime(1990, 1, 1), - }; - var password = "Password123!"; - string? capturedConfirmationLink = null; - - _authRepoMock - .Setup(x => x.GetUserByUsernameAsync(It.IsAny())) - .ReturnsAsync((UserAccount?)null); - - _authRepoMock - .Setup(x => x.GetUserByEmailAsync(It.IsAny())) - .ReturnsAsync((UserAccount?)null); - - _passwordInfraMock - .Setup(x => x.Hash(It.IsAny())) - .Returns("hashed"); - - _authRepoMock - .Setup(x => - x.RegisterUserAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync( - new UserAccount - { - UserAccountId = Guid.NewGuid(), - Username = userAccount.Username, - FirstName = userAccount.FirstName, - LastName = userAccount.LastName, - Email = userAccount.Email, - DateOfBirth = userAccount.DateOfBirth, - } - ); - - _emailTemplateProviderMock - .Setup(x => - x.RenderUserRegisteredEmailAsync( - It.IsAny(), - It.IsAny() - ) - ) - .Callback( - (_, link) => capturedConfirmationLink = link - ) - .ReturnsAsync(""); - - // Act - await _registerService.RegisterAsync(userAccount, password); - - // Assert - capturedConfirmationLink.Should().NotBeNull(); - capturedConfirmationLink - .Should() - .Contain(Uri.EscapeDataString(userAccount.Email)); - } - - [Fact] - public async Task RegisterAsync_WhenEmailSendingFails_ExceptionPropagates() - { - // Arrange - var userAccount = new UserAccount - { - Username = "testuser", - FirstName = "Test", - LastName = "User", - Email = "test@example.com", - DateOfBirth = new DateTime(1990, 1, 1), - }; - var password = "Password123!"; - - _authRepoMock - .Setup(x => x.GetUserByUsernameAsync(It.IsAny())) - .ReturnsAsync((UserAccount?)null); - - _authRepoMock - .Setup(x => x.GetUserByEmailAsync(It.IsAny())) - .ReturnsAsync((UserAccount?)null); - - _passwordInfraMock - .Setup(x => x.Hash(It.IsAny())) - .Returns("hashed"); - - _authRepoMock - .Setup(x => - x.RegisterUserAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync( - new UserAccount - { - UserAccountId = Guid.NewGuid(), - Email = userAccount.Email, - } - ); - - _emailTemplateProviderMock - .Setup(x => - x.RenderUserRegisteredEmailAsync( - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync(""); - - _emailProviderMock - .Setup(x => - x.SendAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) - .ThrowsAsync( - new InvalidOperationException("SMTP server unavailable") - ); - - // Act - var act = async () => await _registerService.RegisterAsync(userAccount, password); - - // Assert - await act.Should() - .ThrowAsync() - .WithMessage("SMTP server unavailable"); - } } diff --git a/src/Core/Service/Service.Auth/ILoginService.cs b/src/Core/Service/Service.Auth/ILoginService.cs index f0bbf84..0e0fa33 100644 --- a/src/Core/Service/Service.Auth/ILoginService.cs +++ b/src/Core/Service/Service.Auth/ILoginService.cs @@ -4,5 +4,5 @@ namespace Service.Auth; public interface ILoginService { - Task LoginAsync(string username, string password); + Task LoginAsync(string username, string password); } diff --git a/src/Core/Service/Service.Auth/IRegisterService.cs b/src/Core/Service/Service.Auth/IRegisterService.cs index efb05b1..1cda5dc 100644 --- a/src/Core/Service/Service.Auth/IRegisterService.cs +++ b/src/Core/Service/Service.Auth/IRegisterService.cs @@ -2,7 +2,16 @@ using Domain.Entities; namespace Service.Auth; +public record AuthServiceReturn( + UserAccount UserAccount, + string RefreshToken, + string AccessToken +); + public interface IRegisterService { - Task RegisterAsync(UserAccount userAccount, string password); + Task RegisterAsync( + UserAccount userAccount, + string password + ); } diff --git a/src/Core/Service/Service.Auth/ITokenService.cs b/src/Core/Service/Service.Auth/ITokenService.cs new file mode 100644 index 0000000..3d2f0f8 --- /dev/null +++ b/src/Core/Service/Service.Auth/ITokenService.cs @@ -0,0 +1,34 @@ +using Domain.Entities; +using Infrastructure.Jwt; + +namespace Service.Auth; + +public interface ITokenService +{ + public string GenerateAccessToken(UserAccount user); + public string GenerateRefreshToken(UserAccount user); +} + +public class TokenService(ITokenInfrastructure tokenInfrastructure) + : ITokenService +{ + public string GenerateAccessToken(UserAccount userAccount) + { + var jwtExpiresAt = DateTime.UtcNow.AddHours(1); + return tokenInfrastructure.GenerateJwt( + userAccount.UserAccountId, + userAccount.Username, + jwtExpiresAt + ); + } + + public string GenerateRefreshToken(UserAccount userAccount) + { + var jwtExpiresAt = DateTime.UtcNow.AddDays(21); + return tokenInfrastructure.GenerateJwt( + userAccount.UserAccountId, + userAccount.Username, + jwtExpiresAt + ); + } +} diff --git a/src/Core/Service/Service.Auth/LoginService.cs b/src/Core/Service/Service.Auth/LoginService.cs index 2cb0902..1e0115b 100644 --- a/src/Core/Service/Service.Auth/LoginService.cs +++ b/src/Core/Service/Service.Auth/LoginService.cs @@ -7,28 +7,34 @@ namespace Service.Auth; public class LoginService( IAuthRepository authRepo, - IPasswordInfrastructure passwordInfrastructure + IPasswordInfrastructure passwordInfrastructure, + ITokenService tokenService ) : ILoginService { - - public async Task LoginAsync(string username, string password) + public async Task LoginAsync( + string username, + string password + ) { // Attempt lookup by username - var user = await authRepo.GetUserByUsernameAsync(username); - // the user was not found - if (user is null) - throw new UnauthorizedException("Invalid username or password."); + var user = + await authRepo.GetUserByUsernameAsync(username) + ?? throw new UnauthorizedException("Invalid username or password."); // @todo handle expired passwords - var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId); - - if (activeCred is null) - throw new UnauthorizedException("Invalid username or password."); + var activeCred = + await authRepo.GetActiveCredentialByUserAccountIdAsync( + user.UserAccountId + ) + ?? throw new UnauthorizedException("Invalid username or password."); if (!passwordInfrastructure.Verify(password, activeCred.Hash)) throw new UnauthorizedException("Invalid username or password."); - return user; + string accessToken = tokenService.GenerateAccessToken(user); + string refreshToken = tokenService.GenerateRefreshToken(user); + + return new AuthServiceReturn(user, refreshToken, accessToken); } } diff --git a/src/Core/Service/Service.Auth/RegisterService.cs b/src/Core/Service/Service.Auth/RegisterService.cs index 03bb988..f701a4e 100644 --- a/src/Core/Service/Service.Auth/RegisterService.cs +++ b/src/Core/Service/Service.Auth/RegisterService.cs @@ -9,14 +9,19 @@ namespace Service.Auth; public class RegisterService( IAuthRepository authRepo, - IPasswordInfrastructure passwordInfrastructure + IPasswordInfrastructure passwordInfrastructure, + ITokenService tokenService ) : IRegisterService { private async Task ValidateUserDoesNotExist(UserAccount userAccount) { // Check if user already exists - var existingUsername = await authRepo.GetUserByUsernameAsync(userAccount.Username); - var existingEmail = await authRepo.GetUserByEmailAsync(userAccount.Email); + var existingUsername = await authRepo.GetUserByUsernameAsync( + userAccount.Username + ); + var existingEmail = await authRepo.GetUserByEmailAsync( + userAccount.Email + ); if (existingUsername != null || existingEmail != null) { @@ -24,7 +29,10 @@ public class RegisterService( } } - public async Task RegisterAsync(UserAccount userAccount, string password) + public async Task RegisterAsync( + UserAccount userAccount, + string password + ) { await ValidateUserDoesNotExist(userAccount); // password hashing @@ -37,8 +45,12 @@ public class RegisterService( userAccount.LastName, userAccount.Email, userAccount.DateOfBirth, - hashed); + hashed + ); - return createdUser; + var accessToken = tokenService.GenerateAccessToken(createdUser); + var refreshToken = tokenService.GenerateRefreshToken(createdUser); + + return new AuthServiceReturn(createdUser, refreshToken, accessToken); } } diff --git a/src/Core/Service/Service.Auth/Service.Auth.csproj b/src/Core/Service/Service.Auth/Service.Auth.csproj index b7efbea..6b707d9 100644 --- a/src/Core/Service/Service.Auth/Service.Auth.csproj +++ b/src/Core/Service/Service.Auth/Service.Auth.csproj @@ -6,19 +6,19 @@ - + - - - - + + + + +