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 Microsoft.AspNetCore.Mvc;
using ServiceCore.Services;
namespace WebAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthController(IAuthService auth) : ControllerBase
public class AuthController(IAuthService auth, IJwtService jwtService) : ControllerBase
{
public record RegisterRequest(
string Username,
@@ -39,9 +39,15 @@ namespace WebAPI.Controllers
[HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest req)
{
var ok = await auth.LoginAsync(req.UsernameOrEmail, req.Password);
if (!ok) return Unauthorized();
return Ok(new { success = true });
var userAccount = await auth.LoginAsync(req.UsernameOrEmail, req.Password);
if (userAccount is null)
{
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 Microsoft.AspNetCore.Mvc;
using ServiceCore.Services;
namespace WebAPI.Controllers
{

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
using DataAccessLayer.Entities;
using DataAccessLayer.Repositories.UserAccount;
using DataAccessLayer.Repositories.UserCredential;
namespace BusinessLayer.Services
namespace ServiceCore.Services
{
public class AuthService(IUserAccountRepository userRepo, IUserCredentialRepository credRepo) : IAuthService
{
@@ -26,18 +25,31 @@ namespace BusinessLayer.Services
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
var user = await userRepo.GetByUsernameAsync(usernameOrEmail)
var user = await userRepo.GetByUsernameAsync(usernameOrEmail)
?? await userRepo.GetByEmailAsync(usernameOrEmail);
if (user is null) return false;
// the user was not found
if (user is null)
{
return null;
}
// they don't have an active credential
// @todo handle expired passwords
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)

View File

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

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;
namespace BusinessLayer.Services
namespace ServiceCore.Services
{
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 Konscious.Security.Cryptography;
namespace BusinessLayer.Services
namespace ServiceCore.Services
{
public static class PasswordHasher
{

View File

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