Update auth service, move JWT handling out of controller

This commit is contained in:
Aaron Po
2026-02-15 18:30:33 -05:00
parent 7eb81039aa
commit 05dac79c4e
15 changed files with 244 additions and 332 deletions

View File

@@ -20,12 +20,9 @@
<ItemGroup>
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
<ProjectReference Include="..\..\Service\Service.UserManagement\Service.UserManagement.csproj" />

View File

@@ -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
);

View File

@@ -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<LoginRequest>
{
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");
}
}

View File

@@ -17,38 +17,55 @@ public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
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"
);
}
}

View File

@@ -2,11 +2,11 @@ namespace API.Core.Contracts.Common;
public record ResponseBody<T>
{
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; }
}

View File

@@ -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<ActionResult<UserAccount>> Register([FromBody] RegisterRequest req)
public async Task<ActionResult<UserAccount>> 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<AuthPayload>
{
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<ActionResult> 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<AuthPayload>
{
Message = "Logged in successfully.",
Payload = new AuthPayload(dto, jwt, DateTime.UtcNow, jwtExpiresAt)
});
return Ok(
new ResponseBody<AuthPayload>
{
Message = "Logged in successfully.",
Payload = new AuthPayload(
rtn.UserAccount.UserAccountId,
rtn.UserAccount.Username,
rtn.RefreshToken,
rtn.AccessToken
),
}
);
}
}
}

View File

@@ -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<ISqlConnectionFactory, DefaultSqlConnectionFactory>();
builder.Services.AddSingleton<
ISqlConnectionFactory,
DefaultSqlConnectionFactory
>();
builder.Services.AddScoped<IUserAccountRepository, UserAccountRepository>();
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
@@ -53,6 +56,7 @@ builder.Services.AddScoped<IAuthRepository, AuthRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ILoginService, LoginService>();
builder.Services.AddScoped<IRegisterService, RegisterService>();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<ITokenInfrastructure, JwtInfrastructure>();
builder.Services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();

View File

@@ -11,15 +11,18 @@ public class LoginServiceTest
{
private readonly Mock<IAuthRepository> _authRepoMock;
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly LoginService _loginService;
public LoginServiceTest()
{
_authRepoMock = new Mock<IAuthRepository>();
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
_tokenServiceMock = new Mock<ITokenService>();
_loginService = new LoginService(
_authRepoMock.Object,
_passwordInfraMock.Object
_passwordInfraMock.Object,
_tokenServiceMock.Object
);
}
@@ -63,13 +66,26 @@ public class LoginServiceTest
.Setup(x => x.Verify(It.IsAny<string>(), It.IsAny<string>()))
.Returns(true);
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>()))
.Returns("refresh-token");
// Act
var result = await _loginService.LoginAsync(username, It.IsAny<string>());
var result = await _loginService.LoginAsync(
username,
It.IsAny<string>()
);
// 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),

View File

@@ -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<IAuthRepository> _authRepoMock;
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
private readonly Mock<IEmailProvider> _emailProviderMock;
private readonly Mock<IEmailTemplateProvider> _emailTemplateProviderMock;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly RegisterService _registerService;
public RegisterServiceTest()
{
_authRepoMock = new Mock<IAuthRepository>();
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
_emailProviderMock = new Mock<IEmailProvider>();
_emailTemplateProviderMock = new Mock<IEmailTemplateProvider>();
_tokenServiceMock = new Mock<ITokenService>();
_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 = "<html><body>Welcome!</body></html>";
// 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<string>()
)
)
.ReturnsAsync(expectedEmailHtml);
// Mock: Token generation
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>()))
.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<UserAccount>()))
.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<string>()
),
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<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>()
),
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<string>(),
It.IsAny<string>()
)
)
.ReturnsAsync("<html></html>");
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>()))
.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<string>()))
.ReturnsAsync((UserAccount?)null);
_authRepoMock
.Setup(x => x.GetUserByEmailAsync(It.IsAny<string>()))
.ReturnsAsync((UserAccount?)null);
_passwordInfraMock
.Setup(x => x.Hash(It.IsAny<string>()))
.Returns("hashed");
_authRepoMock
.Setup(x =>
x.RegisterUserAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<DateTime>(),
It.IsAny<string>()
)
)
.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<string>(),
It.IsAny<string>()
)
)
.Callback<string, string>(
(_, link) => capturedConfirmationLink = link
)
.ReturnsAsync("<html></html>");
// 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<string>()))
.ReturnsAsync((UserAccount?)null);
_authRepoMock
.Setup(x => x.GetUserByEmailAsync(It.IsAny<string>()))
.ReturnsAsync((UserAccount?)null);
_passwordInfraMock
.Setup(x => x.Hash(It.IsAny<string>()))
.Returns("hashed");
_authRepoMock
.Setup(x =>
x.RegisterUserAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<DateTime>(),
It.IsAny<string>()
)
)
.ReturnsAsync(
new UserAccount
{
UserAccountId = Guid.NewGuid(),
Email = userAccount.Email,
}
);
_emailTemplateProviderMock
.Setup(x =>
x.RenderUserRegisteredEmailAsync(
It.IsAny<string>(),
It.IsAny<string>()
)
)
.ReturnsAsync("<html></html>");
_emailProviderMock
.Setup(x =>
x.SendAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>()
)
)
.ThrowsAsync(
new InvalidOperationException("SMTP server unavailable")
);
// Act
var act = async () => await _registerService.RegisterAsync(userAccount, password);
// Assert
await act.Should()
.ThrowAsync<InvalidOperationException>()
.WithMessage("SMTP server unavailable");
}
}

View File

@@ -4,5 +4,5 @@ namespace Service.Auth;
public interface ILoginService
{
Task<UserAccount> LoginAsync(string username, string password);
Task<AuthServiceReturn> LoginAsync(string username, string password);
}

View File

@@ -2,7 +2,16 @@ using Domain.Entities;
namespace Service.Auth;
public record AuthServiceReturn(
UserAccount UserAccount,
string RefreshToken,
string AccessToken
);
public interface IRegisterService
{
Task<UserAccount> RegisterAsync(UserAccount userAccount, string password);
Task<AuthServiceReturn> RegisterAsync(
UserAccount userAccount,
string password
);
}

View File

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

View File

@@ -7,28 +7,34 @@ namespace Service.Auth;
public class LoginService(
IAuthRepository authRepo,
IPasswordInfrastructure passwordInfrastructure
IPasswordInfrastructure passwordInfrastructure,
ITokenService tokenService
) : ILoginService
{
public async Task<UserAccount> LoginAsync(string username, string password)
public async Task<AuthServiceReturn> 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);
}
}

View File

@@ -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<UserAccount> RegisterAsync(UserAccount userAccount, string password)
public async Task<AuthServiceReturn> 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);
}
}

View File

@@ -6,19 +6,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference
Include="Konscious.Security.Cryptography.Argon2"
Version="1.3.1"
/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
</ItemGroup>
</Project>