Update namespace organization in service layer

This commit is contained in:
Aaron Po
2026-01-29 18:13:34 -05:00
parent 45f64f613d
commit 97c093c4bc
12 changed files with 92 additions and 29 deletions

View File

@@ -1,12 +1,12 @@
using BusinessLayer.Services;
using DataAccessLayer.Entities; using DataAccessLayer.Entities;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using ServiceCore.Services;
namespace WebAPI.Controllers namespace WebAPI.Controllers
{ {
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class AuthController(IAuthService auth) : ControllerBase public class AuthController(IAuthService auth, IJwtService jwtService) : ControllerBase
{ {
public record RegisterRequest( public record RegisterRequest(
string Username, string Username,
@@ -39,9 +39,15 @@ namespace WebAPI.Controllers
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest req) public async Task<ActionResult> Login([FromBody] LoginRequest req)
{ {
var ok = await auth.LoginAsync(req.UsernameOrEmail, req.Password); var userAccount = await auth.LoginAsync(req.UsernameOrEmail, req.Password);
if (!ok) return Unauthorized(); if (userAccount is null)
return Ok(new { success = true }); {
return Unauthorized();
}
var jwt = jwtService.GenerateJwt(userAccount.UserAccountId, userAccount.Username, userAccount.DateOfBirth);
return Ok(new { AccessToken = jwt, Message = "Logged in successfully." });
} }
} }
} }

View File

@@ -1,6 +1,6 @@
using BusinessLayer.Services;
using DataAccessLayer.Entities; using DataAccessLayer.Entities;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using ServiceCore.Services;
namespace WebAPI.Controllers namespace WebAPI.Controllers
{ {

View File

@@ -1,7 +1,7 @@
using BusinessLayer.Services;
using DataAccessLayer.Repositories.UserAccount; using DataAccessLayer.Repositories.UserAccount;
using DataAccessLayer.Repositories.UserCredential; using DataAccessLayer.Repositories.UserCredential;
using DataAccessLayer.Sql; using DataAccessLayer.Sql;
using ServiceCore.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -16,6 +16,7 @@ builder.Services.AddScoped<IUserAccountRepository, UserAccountRepository>();
builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IUserCredentialRepository, UserCredentialRepository>(); builder.Services.AddScoped<IUserCredentialRepository, UserCredentialRepository>();
builder.Services.AddScoped<IAuthService, AuthService>(); builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IJwtService, JwtService>();
var app = builder.Build(); var app = builder.Build();
app.UseSwagger(); app.UseSwagger();

View File

@@ -14,7 +14,7 @@ namespace DataAccessLayer.Repositories.UserCredential
command.CommandText = "USP_RotateUserCredential"; command.CommandText = "USP_RotateUserCredential";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@UserAccountId", userAccountId); AddParameter(command, "@UserAccountId_", userAccountId);
AddParameter(command, "@Hash", credential.Hash); AddParameter(command, "@Hash", credential.Hash);
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync();
@@ -27,7 +27,7 @@ namespace DataAccessLayer.Repositories.UserCredential
command.CommandText = "USP_GetActiveUserCredentialByUserAccountId"; command.CommandText = "USP_GetActiveUserCredentialByUserAccountId";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@UserAccountId", userAccountId); AddParameter(command, "@UserAccountId_", userAccountId);
await using var reader = await command.ExecuteReaderAsync(); await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
@@ -40,7 +40,7 @@ namespace DataAccessLayer.Repositories.UserCredential
command.CommandText = "USP_InvalidateUserCredential"; command.CommandText = "USP_InvalidateUserCredential";
command.CommandType = CommandType.StoredProcedure; command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@UserAccountId", userAccountId); AddParameter(command, "@UserAccountId_", userAccountId);
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync();
} }

View File

@@ -3,7 +3,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>BusinessLayer</RootNamespace> <RootNamespace>ServiceCore</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,8 +1,7 @@
using DataAccessLayer.Entities; using DataAccessLayer.Entities;
using DataAccessLayer.Repositories.UserAccount; using DataAccessLayer.Repositories.UserAccount;
using DataAccessLayer.Repositories.UserCredential;
namespace BusinessLayer.Services namespace ServiceCore.Services
{ {
public class AuthService(IUserAccountRepository userRepo, IUserCredentialRepository credRepo) : IAuthService public class AuthService(IUserAccountRepository userRepo, IUserCredentialRepository credRepo) : IAuthService
{ {
@@ -26,18 +25,31 @@ namespace BusinessLayer.Services
return userAccount; return userAccount;
} }
public async Task<bool> LoginAsync(string usernameOrEmail, string password) public async Task<UserAccount?> LoginAsync(string usernameOrEmail, string password)
{ {
// Attempt lookup by username, then email // Attempt lookup by username, then email
var user = await userRepo.GetByUsernameAsync(usernameOrEmail) var user = await userRepo.GetByUsernameAsync(usernameOrEmail)
?? await userRepo.GetByEmailAsync(usernameOrEmail); ?? await userRepo.GetByEmailAsync(usernameOrEmail);
// the user was not found
if (user is null) return false; if (user is null)
{
return null;
}
// they don't have an active credential
// @todo handle expired passwords
var activeCred = await credRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId); var activeCred = await credRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
if (activeCred is null) return false; if (activeCred is null)
{
return null;
}
return PasswordHasher.Verify(password, activeCred.Hash); if (!PasswordHasher.Verify(password, activeCred.Hash))
{
return null;
}
return user;
} }
public async Task InvalidateAsync(Guid userAccountId) public async Task InvalidateAsync(Guid userAccountId)

View File

@@ -1,11 +1,10 @@
using DataAccessLayer.Entities; using DataAccessLayer.Entities;
namespace BusinessLayer.Services namespace ServiceCore.Services
{ {
public interface IAuthService public interface IAuthService
{ {
Task<UserAccount> RegisterAsync(UserAccount userAccount, string password); Task<UserAccount> RegisterAsync(UserAccount userAccount, string password);
Task<bool> LoginAsync(string usernameOrEmail, string password); Task<UserAccount?> LoginAsync(string usernameOrEmail, string password);
Task InvalidateAsync(Guid userAccountId);
} }
} }

View File

@@ -0,0 +1,6 @@
namespace ServiceCore.Services;
public interface IJwtService
{
string GenerateJwt(Guid userId, string username, DateTime expiry);
}

View File

@@ -1,6 +1,8 @@
using DataAccessLayer.Entities; using DataAccessLayer.Entities;
namespace BusinessLayer.Services namespace ServiceCore.Services
{ {
public interface IUserService public interface IUserService
{ {

View File

@@ -0,0 +1,38 @@
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace ServiceCore.Services;
public class JwtService(IConfiguration config) : IJwtService
{
private readonly string? _secret = config["Jwt:Secret"];
public string GenerateJwt(Guid userId, string username, DateTime expiry)
{
var handler = new JsonWebTokenHandler();
var key = Encoding.UTF8.GetBytes(_secret ?? throw new InvalidOperationException("secret not set"));
// Base claims (always present)
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.UniqueName, username),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expiry,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256)
};
return handler.CreateToken(tokenDescriptor);
}
}

View File

@@ -2,7 +2,7 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using Konscious.Security.Cryptography; using Konscious.Security.Cryptography;
namespace BusinessLayer.Services namespace ServiceCore.Services
{ {
public static class PasswordHasher public static class PasswordHasher
{ {

View File

@@ -1,8 +1,7 @@
using DataAccessLayer.Entities; using DataAccessLayer.Entities;
using DataAccessLayer.Repositories;
using DataAccessLayer.Repositories.UserAccount; using DataAccessLayer.Repositories.UserAccount;
namespace BusinessLayer.Services namespace ServiceCore.Services
{ {
public class UserService(IUserAccountRepository repository) : IUserService public class UserService(IUserAccountRepository repository) : IUserService
{ {