mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Update exception handling (#146)
This commit is contained in:
@@ -18,7 +18,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.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.Auth\Service.Auth.csproj" />
|
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
|
||||||
|
|||||||
@@ -44,13 +44,6 @@ namespace API.Core.Controllers
|
|||||||
public async Task<ActionResult> Login([FromBody] LoginRequest req)
|
public async Task<ActionResult> Login([FromBody] LoginRequest req)
|
||||||
{
|
{
|
||||||
var userAccount = await login.LoginAsync(req.Username, req.Password);
|
var userAccount = await login.LoginAsync(req.Username, req.Password);
|
||||||
if (userAccount is null)
|
|
||||||
{
|
|
||||||
return Unauthorized(new ResponseBody
|
|
||||||
{
|
|
||||||
Message = "Invalid username or password."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
UserDTO dto = new(userAccount.UserAccountId, userAccount.Username);
|
UserDTO dto = new(userAccount.UserAccountId, userAccount.Username);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ namespace API.Core.Controllers
|
|||||||
public async Task<ActionResult<UserAccount>> GetById(Guid id)
|
public async Task<ActionResult<UserAccount>> GetById(Guid id)
|
||||||
{
|
{
|
||||||
var user = await userService.GetByIdAsync(id);
|
var user = await userService.GetByIdAsync(id);
|
||||||
if (user is null) return NotFound();
|
|
||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
||||||
COPY ["Domain/Domain.csproj", "Domain/"]
|
COPY ["Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"]
|
||||||
|
COPY ["Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||||
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
||||||
|
|||||||
84
src/Core/API/API.Core/GlobalException.cs
Normal file
84
src/Core/API/API.Core/GlobalException.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// API.Core/Filters/GlobalExceptionFilter.cs
|
||||||
|
|
||||||
|
using API.Core.Contracts.Common;
|
||||||
|
using Domain.Exceptions;
|
||||||
|
using FluentValidation;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace API.Core;
|
||||||
|
|
||||||
|
public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger) : IExceptionFilter
|
||||||
|
{
|
||||||
|
public void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
logger.LogError(context.Exception, "Unhandled exception occurred");
|
||||||
|
|
||||||
|
switch (context.Exception)
|
||||||
|
{
|
||||||
|
case FluentValidation.ValidationException fluentValidationException:
|
||||||
|
var errors = fluentValidationException.Errors
|
||||||
|
.GroupBy(e => e.PropertyName)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.Select(e => e.ErrorMessage).ToArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
context.Result = new BadRequestObjectResult(new
|
||||||
|
{
|
||||||
|
message = "Validation failed",
|
||||||
|
errors
|
||||||
|
});
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConflictException ex:
|
||||||
|
context.Result = new ObjectResult(new ResponseBody { Message = ex.Message })
|
||||||
|
{
|
||||||
|
StatusCode = 409
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotFoundException ex:
|
||||||
|
context.Result = new ObjectResult(new ResponseBody { Message = ex.Message })
|
||||||
|
{
|
||||||
|
StatusCode = 404
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UnauthorizedException ex:
|
||||||
|
context.Result = new ObjectResult(new ResponseBody { Message = ex.Message })
|
||||||
|
{
|
||||||
|
StatusCode = 401
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ForbiddenException ex:
|
||||||
|
context.Result = new ObjectResult(new ResponseBody { Message = ex.Message })
|
||||||
|
{
|
||||||
|
StatusCode = 403
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Domain.Exceptions.ValidationException ex:
|
||||||
|
context.Result = new ObjectResult(new ResponseBody { Message = ex.Message })
|
||||||
|
{
|
||||||
|
StatusCode = 400
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
context.Result = new ObjectResult(new ResponseBody { Message = "An unexpected error occurred" })
|
||||||
|
{
|
||||||
|
StatusCode = 500
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.Json;
|
|
||||||
using API.Core.Contracts.Common;
|
|
||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace API.Core.Middleware;
|
|
||||||
|
|
||||||
public class ValidationExceptionHandlingMiddleware(RequestDelegate next)
|
|
||||||
{
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await next(context);
|
|
||||||
}
|
|
||||||
catch (ValidationException ex)
|
|
||||||
{
|
|
||||||
await HandleValidationExceptionAsync(context, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task HandleValidationExceptionAsync(HttpContext context, ValidationException exception)
|
|
||||||
{
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
|
||||||
|
|
||||||
var errors = exception.Errors
|
|
||||||
.Select(e => e.ErrorMessage)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var message = errors.Count == 1
|
|
||||||
? errors[0]
|
|
||||||
: "Validation failed. " + string.Join(" ", errors);
|
|
||||||
|
|
||||||
var response = new ResponseBody
|
|
||||||
{
|
|
||||||
Message = message
|
|
||||||
};
|
|
||||||
|
|
||||||
var jsonOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
|
|
||||||
return context.Response.WriteAsync(JsonSerializer.Serialize(response, jsonOptions));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using API.Core;
|
||||||
|
using Domain.Exceptions;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.AspNetCore;
|
using FluentValidation.AspNetCore;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
@@ -6,34 +8,19 @@ 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 Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Service.Auth.Auth;
|
using Service.Auth.Auth;
|
||||||
using Service.UserManagement.User;
|
using Service.UserManagement.User;
|
||||||
|
using API.Core.Contracts.Common;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddControllers()
|
// Global Exception Filter
|
||||||
.ConfigureApiBehaviorOptions(options =>
|
builder.Services.AddControllers(options =>
|
||||||
{
|
{
|
||||||
options.InvalidModelStateResponseFactory = context =>
|
options.Filters.Add<GlobalExceptionFilter>();
|
||||||
{
|
|
||||||
var errors = context.ModelState.Values
|
|
||||||
.SelectMany(v => v.Errors)
|
|
||||||
.Select(e => e.ErrorMessage)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var message = errors.Count == 1
|
|
||||||
? errors[0]
|
|
||||||
: string.Join(" ", errors);
|
|
||||||
|
|
||||||
var response = new
|
|
||||||
{
|
|
||||||
message
|
|
||||||
};
|
|
||||||
|
|
||||||
return new BadRequestObjectResult(response);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
@@ -67,6 +54,8 @@ builder.Services.AddScoped<IRegisterService, RegisterService>();
|
|||||||
builder.Services.AddScoped<ITokenInfrastructure, JwtInfrastructure>();
|
builder.Services.AddScoped<ITokenInfrastructure, JwtInfrastructure>();
|
||||||
builder.Services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();
|
builder.Services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();
|
||||||
|
|
||||||
|
// Register the exception filter
|
||||||
|
builder.Services.AddScoped<GlobalExceptionFilter>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -90,6 +79,3 @@ lifetime.ApplicationStopping.Register(() =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
// Make Program class accessible to test projects
|
|
||||||
public partial class Program { }
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ ARG BUILD_CONFIGURATION=Release
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
||||||
COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"]
|
COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"]
|
||||||
COPY ["Domain/Domain.csproj", "Domain/"]
|
COPY ["Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"]
|
||||||
|
COPY ["Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||||
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
||||||
|
|||||||
@@ -12,25 +12,19 @@ Feature: User Registration
|
|||||||
And the response JSON should have "message" equal "User registered successfully."
|
And the response JSON should have "message" equal "User registered successfully."
|
||||||
And the response JSON should have an access token
|
And the response JSON should have an access token
|
||||||
|
|
||||||
@Ignore
|
|
||||||
Scenario: Registration fails with existing username
|
Scenario: Registration fails with existing username
|
||||||
Given the API is running
|
Given the API is running
|
||||||
And I have an existing account with username "existinguser"
|
|
||||||
When I submit a registration request with values:
|
When I submit a registration request with values:
|
||||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||||
| existinguser | Existing | User | existing@example.com | 1990-01-01 | Password1! |
|
| test.user | Test | User | example@example.com | 2001-11-11 | Password1! |
|
||||||
Then the response has HTTP status 409
|
Then the response has HTTP status 409
|
||||||
And the response JSON should have "message" equal "Username already exists."
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
Scenario: Registration fails with existing email
|
Scenario: Registration fails with existing email
|
||||||
Given the API is running
|
Given the API is running
|
||||||
And I have an existing account with email "existing@example.com"
|
|
||||||
When I submit a registration request with values:
|
When I submit a registration request with values:
|
||||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||||
| newuser | New | User | existing@example.com | 1990-01-01 | Password1! |
|
| newuser | New | User | test.user@thebiergarten.app | 1990-01-01 | Password1! |
|
||||||
Then the response has HTTP status 409
|
Then the response has HTTP status 409
|
||||||
And the response JSON should have "message" equal "Email already in use."
|
|
||||||
|
|
||||||
Scenario: Registration fails with missing required fields
|
Scenario: Registration fails with missing required fields
|
||||||
Given the API is running
|
Given the API is running
|
||||||
|
|||||||
@@ -201,18 +201,6 @@ public class AuthSteps(ScenarioContext scenario)
|
|||||||
scenario[ResponseBodyKey] = responseBody;
|
scenario[ResponseBodyKey] = responseBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Given("I have an existing account with username {string}")]
|
|
||||||
public void GivenIHaveAnExistingAccountWithUsername(string username)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Given("I have an existing account with email {string}")]
|
|
||||||
public void GivenIHaveAnExistingAccountWithEmail(string email)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[When("I submit a registration request using a GET request")]
|
[When("I submit a registration request using a GET request")]
|
||||||
public async Task WhenISubmitARegistrationRequestUsingAGetRequest()
|
public async Task WhenISubmitARegistrationRequestUsingAGetRequest()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
<Project Path="Database/Database.Seed/Database.Seed.csproj" />
|
<Project Path="Database/Database.Seed/Database.Seed.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Domain/">
|
<Folder Name="/Domain/">
|
||||||
<Project Path="Domain/Domain.csproj" />
|
<Project Path="Domain.Entities\Domain.Entities.csproj" />
|
||||||
|
<Project Path="Domain.Exceptions/Domain.Exceptions.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Infrastructure/">
|
<Folder Name="/Infrastructure/">
|
||||||
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ internal class UserSeeder : ISeeder
|
|||||||
int createdCredentials = 0;
|
int createdCredentials = 0;
|
||||||
int createdVerifications = 0;
|
int createdVerifications = 0;
|
||||||
|
|
||||||
|
// create a known user for testing purposes
|
||||||
{
|
{
|
||||||
const string firstName = "Test";
|
const string firstName = "Test";
|
||||||
const string lastName = "User";
|
const string lastName = "User";
|
||||||
|
|||||||
9
src/Core/Domain.Exceptions/Domain.Exceptions.csproj
Normal file
9
src/Core/Domain.Exceptions/Domain.Exceptions.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
33
src/Core/Domain.Exceptions/Exceptions.cs
Normal file
33
src/Core/Domain.Exceptions/Exceptions.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
namespace Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a resource conflict occurs (e.g., duplicate username, email already in use).
|
||||||
|
/// Maps to HTTP 409 Conflict.
|
||||||
|
/// </summary>
|
||||||
|
public class ConflictException(string message) : Exception(message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a requested resource is not found.
|
||||||
|
/// Maps to HTTP 404 Not Found.
|
||||||
|
/// </summary>
|
||||||
|
public class NotFoundException(string message) : Exception(message);
|
||||||
|
|
||||||
|
// Domain.Exceptions/UnauthorizedException.cs
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when authentication fails or is required.
|
||||||
|
/// Maps to HTTP 401 Unauthorized.
|
||||||
|
/// </summary>
|
||||||
|
public class UnauthorizedException(string message) : Exception(message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a user is authenticated but lacks permission to access a resource.
|
||||||
|
/// Maps to HTTP 403 Forbidden.
|
||||||
|
/// </summary>
|
||||||
|
public class ForbiddenException(string message) : Exception(message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when business rule validation fails (distinct from FluentValidation).
|
||||||
|
/// Maps to HTTP 400 Bad Request.
|
||||||
|
/// </summary>
|
||||||
|
public class ValidationException(string message) : Exception(message);
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["Domain/Domain.csproj", "Domain/"]
|
COPY ["Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"]
|
||||||
|
COPY ["Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository.Tests/"]
|
COPY ["Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository.Tests/"]
|
||||||
RUN dotnet restore "Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj"
|
RUN dotnet restore "Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj"
|
||||||
|
|||||||
@@ -18,6 +18,6 @@
|
|||||||
/>
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ namespace Service.Auth.Auth;
|
|||||||
|
|
||||||
public interface ILoginService
|
public interface ILoginService
|
||||||
{
|
{
|
||||||
Task<UserAccount?> LoginAsync(string username, string password);
|
Task<UserAccount> LoginAsync(string username, string password);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Exceptions;
|
||||||
using Infrastructure.PasswordHashing;
|
using Infrastructure.PasswordHashing;
|
||||||
using Infrastructure.Repository.Auth;
|
using Infrastructure.Repository.Auth;
|
||||||
|
|
||||||
@@ -11,18 +12,24 @@ public class LoginService(
|
|||||||
) : ILoginService
|
) : ILoginService
|
||||||
{
|
{
|
||||||
|
|
||||||
public async Task<UserAccount?> LoginAsync(string username, string password)
|
public async Task<UserAccount> LoginAsync(string username, string password)
|
||||||
{
|
{
|
||||||
// Attempt lookup by username
|
// Attempt lookup by username
|
||||||
var user = await authRepo.GetUserByUsernameAsync(username);
|
var user = await authRepo.GetUserByUsernameAsync(username);
|
||||||
|
|
||||||
// the user was not found
|
// the user was not found
|
||||||
if (user is null) return null;
|
if (user is null)
|
||||||
|
throw new UnauthorizedException("Invalid username or password.");
|
||||||
|
|
||||||
// @todo handle expired passwords
|
// @todo handle expired passwords
|
||||||
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
|
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
|
||||||
|
|
||||||
if (activeCred is null) return null;
|
if (activeCred is null)
|
||||||
return !passwordInfrastructure.Verify(password, activeCred.Hash) ? null : user;
|
throw new UnauthorizedException("Invalid username or password.");
|
||||||
|
|
||||||
|
if (!passwordInfrastructure.Verify(password, activeCred.Hash))
|
||||||
|
throw new UnauthorizedException("Invalid username or password.");
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Exceptions;
|
||||||
using Infrastructure.PasswordHashing;
|
using Infrastructure.PasswordHashing;
|
||||||
using Infrastructure.Repository.Auth;
|
using Infrastructure.Repository.Auth;
|
||||||
|
|
||||||
@@ -13,12 +14,16 @@ public class RegisterService(
|
|||||||
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
|
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
|
||||||
{
|
{
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
var user = await authRepo.GetUserByUsernameAsync(userAccount.Username);
|
var existingUsername = await authRepo.GetUserByUsernameAsync(userAccount.Username);
|
||||||
if (user is not null)
|
var existingEmail = await authRepo.GetUserByEmailAsync(userAccount.Email);
|
||||||
|
|
||||||
|
if (existingUsername != null || existingEmail != null)
|
||||||
{
|
{
|
||||||
return null!;
|
throw new ConflictException("Username or email already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// password hashing
|
// password hashing
|
||||||
var hashed = passwordInfrastructure.Hash(password);
|
var hashed = passwordInfrastructure.Hash(password);
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
|
<ProjectReference
|
||||||
|
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
<ProjectReference
|
<ProjectReference
|
||||||
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
|
<ProjectReference
|
||||||
|
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Service.UserManagement.User;
|
|||||||
public interface IUserService
|
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<UserAccount> GetByIdAsync(Guid id);
|
||||||
|
|
||||||
Task UpdateAsync(UserAccount userAccount);
|
Task UpdateAsync(UserAccount userAccount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Exceptions;
|
||||||
using Infrastructure.Repository.UserAccount;
|
using Infrastructure.Repository.UserAccount;
|
||||||
|
|
||||||
namespace Service.UserManagement.User;
|
namespace Service.UserManagement.User;
|
||||||
@@ -10,9 +11,12 @@ public class UserService(IUserAccountRepository repository) : IUserService
|
|||||||
return await repository.GetAllAsync(limit, offset);
|
return await repository.GetAllAsync(limit, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserAccount?> GetByIdAsync(Guid id)
|
public async Task<UserAccount> GetByIdAsync(Guid id)
|
||||||
{
|
{
|
||||||
return await repository.GetByIdAsync(id);
|
var user = await repository.GetByIdAsync(id);
|
||||||
|
if (user is null)
|
||||||
|
throw new NotFoundException($"User with ID {id} not found");
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAsync(UserAccount userAccount)
|
public async Task UpdateAsync(UserAccount userAccount)
|
||||||
|
|||||||
Reference in New Issue
Block a user