Refactor auth/user services

This commit is contained in:
Aaron Po
2026-02-12 19:28:40 -05:00
parent caf13de36e
commit 954c9c389c
18 changed files with 98 additions and 55 deletions

View File

@@ -21,7 +21,8 @@
<ProjectReference Include="..\..\Domain\Domain.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Service\Service.Core\Service.Core.csproj" />
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
<ProjectReference Include="..\..\Service\Service.UserManagement\Service.UserManagement.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,20 +3,18 @@ using API.Core.Contracts.Common;
using Domain.Entities;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Mvc;
using Service.Core.Auth;
using Service.Auth.Auth;
namespace API.Core.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthController(IAuthService auth, IJwtService jwtService) : ControllerBase
public class AuthController(IRegisterService register, ILoginService login, ITokenInfrastructure tokenInfrastructure) : ControllerBase
{
[HttpPost("register")]
public async Task<ActionResult<UserAccount>> Register([FromBody] RegisterRequest req)
{
var created = await auth.RegisterAsync(new UserAccount
var created = await register.RegisterAsync(new UserAccount
{
UserAccountId = Guid.Empty,
Username = req.Username,
@@ -27,8 +25,7 @@ namespace API.Core.Controllers
}, req.Password);
var jwtExpiresAt = DateTime.UtcNow.AddHours(1);
var jwt = jwtService.GenerateJwt(created.UserAccountId, created.Username, jwtExpiresAt
var jwt = tokenInfrastructure.GenerateJwt(created.UserAccountId, created.Username, jwtExpiresAt
);
var response = new ResponseBody<AuthPayload>
@@ -46,7 +43,7 @@ namespace API.Core.Controllers
[HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest req)
{
var userAccount = await auth.LoginAsync(req.Username, req.Password);
var userAccount = await login.LoginAsync(req.Username, req.Password);
if (userAccount is null)
{
return Unauthorized(new ResponseBody
@@ -58,7 +55,7 @@ namespace API.Core.Controllers
UserDTO dto = new(userAccount.UserAccountId, userAccount.Username);
var jwtExpiresAt = DateTime.UtcNow.AddHours(1);
var jwt = jwtService.GenerateJwt(userAccount.UserAccountId, userAccount.Username, jwtExpiresAt);
var jwt = tokenInfrastructure.GenerateJwt(userAccount.UserAccountId, userAccount.Username, jwtExpiresAt);
return Ok(new ResponseBody<AuthPayload>
{

View File

@@ -1,6 +1,6 @@
using Domain.Entities;
using Microsoft.AspNetCore.Mvc;
using Service.Core.User;
using Service.UserManagement.User;
namespace API.Core.Controllers
{

View File

@@ -6,8 +6,9 @@ using Infrastructure.Repository.Auth;
using Infrastructure.Repository.Sql;
using Infrastructure.Repository.UserAccount;
using Microsoft.AspNetCore.Mvc;
using Service.Core.Auth;
using Service.Core.User;
using Service.Auth.Auth;
using Service.UserManagement.User;
var builder = WebApplication.CreateBuilder(args);
@@ -52,14 +53,20 @@ if (!builder.Environment.IsProduction())
builder.Logging.AddDebug();
}
// Dependency Injection
// Configure Dependency Injection -------------------------------------------------------------------------------------
builder.Services.AddSingleton<ISqlConnectionFactory, DefaultSqlConnectionFactory>();
builder.Services.AddScoped<IUserAccountRepository, UserAccountRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IJwtService, JwtService>();
builder.Services.AddScoped<IPasswordInfra, Argon2Infrastructure>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ILoginService, LoginService>();
builder.Services.AddScoped<IRegisterService, RegisterService>();
builder.Services.AddScoped<ITokenInfrastructure, JwtInfrastructure>();
builder.Services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();
var app = builder.Build();

View File

@@ -17,6 +17,7 @@
<Project Path="Infrastructure\Infrastructure.Repository.Tests\Infrastructure.Repository.Tests.csproj" />
</Folder>
<Folder Name="/Service/">
<Project Path="Service/Service.Core/Service.Core.csproj" />
<Project Path="Service/Service.UserManagement/Service.UserManagement.csproj" />
<Project Path="Service\Service.Auth\Service.Auth.csproj" />
</Folder>
</Solution>

View File

@@ -1,6 +1,6 @@
namespace Infrastructure.Jwt;
public interface IJwtService
public interface ITokenInfrastructure
{
string GenerateJwt(Guid userId, string username, DateTime expiry);
}

View File

@@ -6,7 +6,7 @@ using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredCla
namespace Infrastructure.Jwt;
public class JwtService : IJwtService
public class JwtInfrastructure : ITokenInfrastructure
{
private readonly string? _secret = Environment.GetEnvironmentVariable(
"JWT_SECRET"

View File

@@ -4,7 +4,7 @@ using Konscious.Security.Cryptography;
namespace Infrastructure.PasswordHashing;
public class Argon2Infrastructure : IPasswordInfra
public class Argon2Infrastructure : IPasswordInfrastructure
{
private const int SaltSize = 16; // 128-bit
private const int HashSize = 32; // 256-bit

View File

@@ -1,6 +1,6 @@
namespace Infrastructure.PasswordHashing;
public interface IPasswordInfra
public interface IPasswordInfrastructure
{
public string Hash(string password);
public bool Verify(string password, string stored);

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using Domain.Entities;
namespace Service.Auth.Auth;
public interface ILoginService
{
Task<UserAccount?> LoginAsync(string username, string password);
}

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using Domain.Entities;
namespace Service.Auth.Auth;
public interface IRegisterService
{
Task<UserAccount> RegisterAsync(UserAccount userAccount, string password);
}

View File

@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Domain.Entities;
using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth;
namespace Service.Auth.Auth;
public class LoginService(
IAuthRepository authRepo,
IPasswordInfrastructure passwordInfrastructure
) : ILoginService
{
public async Task<UserAccount?> LoginAsync(string username, string password)
{
// Attempt lookup by username
var user = await authRepo.GetUserByUsernameAsync(username);
// the user was not found
if (user is null) return null;
// @todo handle expired passwords
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
if (activeCred is null) return null;
return !passwordInfrastructure.Verify(password, activeCred.Hash) ? null : user;
}
}

View File

@@ -1,13 +1,14 @@
using System.Threading.Tasks;
using Domain.Entities;
using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth;
namespace Service.Core.Auth;
namespace Service.Auth.Auth;
public class AuthService(
public class RegisterService(
IAuthRepository authRepo,
IPasswordInfra passwordInfra
) : IAuthService
IPasswordInfrastructure passwordInfrastructure
) : IRegisterService
{
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
{
@@ -19,7 +20,7 @@ public class AuthService(
}
// password hashing
var hashed = passwordInfra.Hash(password);
var hashed = passwordInfrastructure.Hash(password);
// Register user with hashed password
return await authRepo.RegisterUserAsync(
@@ -31,18 +32,5 @@ public class AuthService(
hashed);
}
public async Task<UserAccount?> LoginAsync(string username, string password)
{
// Attempt lookup by username
var user = await authRepo.GetUserByUsernameAsync(username);
// the user was not found
if (user is null) return null;
// @todo handle expired passwords
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
if (activeCred is null) return null;
return !passwordInfra.Verify(password, activeCred.Hash) ? null : user;
}
}

View File

@@ -3,7 +3,6 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Service.Core</RootNamespace>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,9 +0,0 @@
using Domain.Entities;
namespace Service.Core.Auth;
public interface IAuthService
{
Task<UserAccount> RegisterAsync(UserAccount userAccount, string password);
Task<UserAccount?> LoginAsync(string username, string password);
}

View 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="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
using Domain.Entities;
namespace Service.Core.User;
namespace Service.UserManagement.User;
public interface IUserService
{

View File

@@ -1,7 +1,7 @@
using Domain.Entities;
using Infrastructure.Repository.UserAccount;
namespace Service.Core.User;
namespace Service.UserManagement.User;
public class UserService(IUserAccountRepository repository) : IUserService
{