mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-06 02:19:05 +00:00
Service refactor (#153)
* remove email out of register service * Update auth service, move JWT handling out of controller * add docker config for service auth test * Update mock email system * Format: ./src/Core/Service * Refactor authentication payloads and services for registration and login processes * Format: src/Core/API, src/Core/Service
This commit is contained in:
21
src/Core/Service/Service.Auth.Tests/Dockerfile
Normal file
21
src/Core/Service/Service.Auth.Tests/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"]
|
||||
COPY ["Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"]
|
||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
||||
COPY ["Service/Service.Auth/Service.Auth.csproj", "Service/Service.Auth/"]
|
||||
COPY ["Service/Service.Auth.Tests/Service.Auth.Tests.csproj", "Service/Service.Auth.Tests/"]
|
||||
RUN dotnet restore "Service/Service.Auth.Tests/Service.Auth.Tests.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Service/Service.Auth.Tests"
|
||||
RUN dotnet build "./Service.Auth.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS final
|
||||
RUN mkdir -p /app/test-results/service-auth-tests
|
||||
WORKDIR /src/Service/Service.Auth.Tests
|
||||
ENTRYPOINT ["dotnet", "test", "./Service.Auth.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/service-auth-tests/results.trx"]
|
||||
@@ -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),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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;
|
||||
using Service.Emails;
|
||||
|
||||
namespace Service.Auth.Tests;
|
||||
|
||||
@@ -13,27 +12,27 @@ 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 Mock<IEmailService> _emailServiceMock; // todo handle email related test cases here
|
||||
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>();
|
||||
_emailServiceMock = new Mock<IEmailService>();
|
||||
|
||||
_registerService = new RegisterService(
|
||||
_authRepoMock.Object,
|
||||
_passwordInfraMock.Object,
|
||||
_emailProviderMock.Object,
|
||||
_emailTemplateProviderMock.Object
|
||||
_tokenServiceMock.Object,
|
||||
_emailServiceMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_WithValidData_CreatesUserAndSendsEmail()
|
||||
public async Task RegisterAsync_WithValidData_CreatesUserAndReturnsAuthServiceReturn()
|
||||
{
|
||||
// Arrange
|
||||
var userAccount = new UserAccount
|
||||
@@ -48,7 +47,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 +87,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 +132,14 @@ public class RegisterServiceTest
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
_emailTemplateProviderMock.Verify(
|
||||
_emailServiceMock.Verify(
|
||||
x =>
|
||||
x.RenderUserRegisteredEmailAsync(
|
||||
userAccount.FirstName,
|
||||
x.SendRegistrationEmailAsync(
|
||||
It.IsAny<UserAccount>(),
|
||||
It.IsAny<string>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
_emailProviderMock.Verify(
|
||||
x =>
|
||||
x.SendAsync(
|
||||
userAccount.Email,
|
||||
"Welcome to The Biergarten App!",
|
||||
expectedEmailHtml,
|
||||
true
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -195,7 +175,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 +196,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 +231,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 +293,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 +319,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ namespace Service.Auth;
|
||||
|
||||
public interface ILoginService
|
||||
{
|
||||
Task<UserAccount> LoginAsync(string username, string password);
|
||||
Task<LoginServiceReturn> LoginAsync(string username, string password);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,38 @@ using Domain.Entities;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
public record RegisterServiceReturn
|
||||
{
|
||||
public bool IsAuthenticated { get; init; } = false;
|
||||
public bool EmailSent { get; init; } = false;
|
||||
public UserAccount UserAccount { get; init; }
|
||||
public string AccessToken { get; init; } = string.Empty;
|
||||
public string RefreshToken { get; init; } = string.Empty;
|
||||
|
||||
public RegisterServiceReturn(
|
||||
UserAccount userAccount,
|
||||
string accessToken,
|
||||
string refreshToken,
|
||||
bool emailSent
|
||||
)
|
||||
{
|
||||
IsAuthenticated = true;
|
||||
EmailSent = emailSent;
|
||||
UserAccount = userAccount;
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public RegisterServiceReturn(UserAccount userAccount)
|
||||
{
|
||||
UserAccount = userAccount;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRegisterService
|
||||
{
|
||||
Task<UserAccount> RegisterAsync(UserAccount userAccount, string password);
|
||||
Task<RegisterServiceReturn> RegisterAsync(
|
||||
UserAccount userAccount,
|
||||
string password
|
||||
);
|
||||
}
|
||||
|
||||
34
src/Core/Service/Service.Auth/ITokenService.cs
Normal file
34
src/Core/Service/Service.Auth/ITokenService.cs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,42 @@ using Infrastructure.Repository.Auth;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
public record LoginServiceReturn(
|
||||
UserAccount UserAccount,
|
||||
string RefreshToken,
|
||||
string AccessToken
|
||||
);
|
||||
|
||||
public class LoginService(
|
||||
IAuthRepository authRepo,
|
||||
IPasswordInfrastructure passwordInfrastructure
|
||||
IPasswordInfrastructure passwordInfrastructure,
|
||||
ITokenService tokenService
|
||||
) : ILoginService
|
||||
{
|
||||
|
||||
public async Task<UserAccount> LoginAsync(string username, string password)
|
||||
public async Task<LoginServiceReturn> 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 LoginServiceReturn(user, refreshToken, accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,40 @@ using Infrastructure.Email;
|
||||
using Infrastructure.Email.Templates.Rendering;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Infrastructure.Repository.Auth;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Service.Emails;
|
||||
|
||||
namespace Service.Auth;
|
||||
|
||||
public class RegisterService(
|
||||
IAuthRepository authRepo,
|
||||
IPasswordInfrastructure passwordInfrastructure,
|
||||
IEmailProvider emailProvider,
|
||||
IEmailTemplateProvider emailTemplateProvider
|
||||
ITokenService tokenService,
|
||||
IEmailService emailService
|
||||
) : IRegisterService
|
||||
{
|
||||
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
|
||||
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)
|
||||
{
|
||||
throw new ConflictException("Username or email already exists");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<RegisterServiceReturn> RegisterAsync(
|
||||
UserAccount userAccount,
|
||||
string password
|
||||
)
|
||||
{
|
||||
await ValidateUserDoesNotExist(userAccount);
|
||||
// password hashing
|
||||
var hashed = passwordInfrastructure.Hash(password);
|
||||
|
||||
@@ -36,26 +48,41 @@ public class RegisterService(
|
||||
userAccount.LastName,
|
||||
userAccount.Email,
|
||||
userAccount.DateOfBirth,
|
||||
hashed);
|
||||
|
||||
|
||||
// Generate confirmation link (TODO: implement proper token-based confirmation)
|
||||
var confirmationLink = $"https://thebiergarten.app/confirm?email={Uri.EscapeDataString(createdUser.Email)}";
|
||||
|
||||
// Render email template
|
||||
var emailHtml = await emailTemplateProvider.RenderUserRegisteredEmailAsync(
|
||||
createdUser.FirstName,
|
||||
confirmationLink
|
||||
hashed
|
||||
);
|
||||
|
||||
// Send welcome email with rendered template
|
||||
await emailProvider.SendAsync(
|
||||
createdUser.Email,
|
||||
"Welcome to The Biergarten App!",
|
||||
emailHtml,
|
||||
isHtml: true
|
||||
);
|
||||
var accessToken = tokenService.GenerateAccessToken(createdUser);
|
||||
var refreshToken = tokenService.GenerateRefreshToken(createdUser);
|
||||
|
||||
return createdUser;
|
||||
if (
|
||||
string.IsNullOrEmpty(accessToken)
|
||||
|| string.IsNullOrEmpty(refreshToken)
|
||||
)
|
||||
{
|
||||
return new RegisterServiceReturn(createdUser);
|
||||
}
|
||||
|
||||
bool emailSent = false;
|
||||
try
|
||||
{
|
||||
// send confirmation email
|
||||
await emailService.SendRegistrationEmailAsync(
|
||||
createdUser,
|
||||
"some-confirmation-token"
|
||||
);
|
||||
|
||||
emailSent = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return new RegisterServiceReturn(
|
||||
createdUser,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
emailSent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,14 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
<ProjectReference Include="..\Service.Emails\Service.Emails.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
41
src/Core/Service/Service.Emails/EmailService.cs
Normal file
41
src/Core/Service/Service.Emails/EmailService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Domain.Entities;
|
||||
using Infrastructure.Email;
|
||||
using Infrastructure.Email.Templates.Rendering;
|
||||
|
||||
namespace Service.Emails;
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
public Task SendRegistrationEmailAsync(
|
||||
UserAccount createdUser,
|
||||
string confirmationToken
|
||||
);
|
||||
}
|
||||
|
||||
public class EmailService(
|
||||
IEmailProvider emailProvider,
|
||||
IEmailTemplateProvider emailTemplateProvider
|
||||
) : IEmailService
|
||||
{
|
||||
public async Task SendRegistrationEmailAsync(
|
||||
UserAccount createdUser,
|
||||
string confirmationToken
|
||||
)
|
||||
{
|
||||
var confirmationLink =
|
||||
$"https://thebiergarten.app/confirm?token={confirmationToken}";
|
||||
|
||||
var emailHtml =
|
||||
await emailTemplateProvider.RenderUserRegisteredEmailAsync(
|
||||
createdUser.FirstName,
|
||||
confirmationLink
|
||||
);
|
||||
|
||||
await emailProvider.SendAsync(
|
||||
createdUser.Email,
|
||||
"Welcome to The Biergarten App!",
|
||||
emailHtml,
|
||||
isHtml: true
|
||||
);
|
||||
}
|
||||
}
|
||||
13
src/Core/Service/Service.Emails/Service.Emails.csproj
Normal file
13
src/Core/Service/Service.Emails/Service.Emails.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,15 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||
<ProjectReference
|
||||
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -4,7 +4,10 @@ namespace Service.UserManagement.User;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<IEnumerable<UserAccount>> GetAllAsync(int? limit = null, int? offset = null);
|
||||
Task<IEnumerable<UserAccount>> GetAllAsync(
|
||||
int? limit = null,
|
||||
int? offset = null
|
||||
);
|
||||
Task<UserAccount> GetByIdAsync(Guid id);
|
||||
|
||||
Task UpdateAsync(UserAccount userAccount);
|
||||
|
||||
@@ -6,7 +6,10 @@ namespace Service.UserManagement.User;
|
||||
|
||||
public class UserService(IUserAccountRepository repository) : IUserService
|
||||
{
|
||||
public async Task<IEnumerable<UserAccount>> GetAllAsync(int? limit = null, int? offset = null)
|
||||
public async Task<IEnumerable<UserAccount>> GetAllAsync(
|
||||
int? limit = null,
|
||||
int? offset = null
|
||||
)
|
||||
{
|
||||
return await repository.GetAllAsync(limit, offset);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user