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="..\..\Domain\Domain.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" /> <ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.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>
<ItemGroup> <ItemGroup>

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@
<Project Path="Infrastructure\Infrastructure.Repository.Tests\Infrastructure.Repository.Tests.csproj" /> <Project Path="Infrastructure\Infrastructure.Repository.Tests\Infrastructure.Repository.Tests.csproj" />
</Folder> </Folder>
<Folder Name="/Service/"> <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> </Folder>
</Solution> </Solution>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
namespace Infrastructure.PasswordHashing; namespace Infrastructure.PasswordHashing;
public interface IPasswordInfra public interface IPasswordInfrastructure
{ {
public string Hash(string password); public string Hash(string password);
public bool Verify(string password, string stored); 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 Domain.Entities;
using Infrastructure.PasswordHashing; using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth; using Infrastructure.Repository.Auth;
namespace Service.Core.Auth; namespace Service.Auth.Auth;
public class AuthService( public class RegisterService(
IAuthRepository authRepo, IAuthRepository authRepo,
IPasswordInfra passwordInfra IPasswordInfrastructure passwordInfrastructure
) : IAuthService ) : IRegisterService
{ {
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password) public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
{ {
@@ -19,7 +20,7 @@ public class AuthService(
} }
// password hashing // password hashing
var hashed = passwordInfra.Hash(password); var hashed = passwordInfrastructure.Hash(password);
// Register user with hashed password // Register user with hashed password
return await authRepo.RegisterUserAsync( return await authRepo.RegisterUserAsync(
@@ -31,18 +32,5 @@ public class AuthService(
hashed); 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> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Service.Core</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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; using Domain.Entities;
namespace Service.Core.User; namespace Service.UserManagement.User;
public interface IUserService public interface IUserService
{ {

View File

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