mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
Merge Features.Auth and Features.UserManagement into Features.Users
This commit is contained in:
@@ -22,8 +22,7 @@
|
|||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.FileUpload\Infrastructure.FileUpload.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.FileUpload\Infrastructure.FileUpload.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Users\Features.Users.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
<ProjectReference Include="..\..\Features\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ COPY ["Database/Database.Migrations/Database.Migrations.csproj", "Database/Datab
|
|||||||
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
||||||
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
||||||
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"]
|
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"]
|
||||||
COPY ["Features/Features.Auth/Features.Auth.csproj", "Features/Features.Auth/"]
|
|
||||||
COPY ["Features/Features.Auth.Tests/Features.Auth.Tests.csproj", "Features/Features.Auth.Tests/"]
|
|
||||||
COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Features.Breweries/"]
|
COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Features.Breweries/"]
|
||||||
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
||||||
COPY ["Features/Features.Emails/Features.Emails.csproj", "Features/Features.Emails/"]
|
COPY ["Features/Features.Emails/Features.Emails.csproj", "Features/Features.Emails/"]
|
||||||
@@ -32,8 +30,8 @@ COPY ["Features/Features.Locations/Features.Locations.csproj", "Features/Feature
|
|||||||
COPY ["Features/Features.Locations.Tests/Features.Locations.Tests.csproj", "Features/Features.Locations.Tests/"]
|
COPY ["Features/Features.Locations.Tests/Features.Locations.Tests.csproj", "Features/Features.Locations.Tests/"]
|
||||||
COPY ["Features/Features.PhotoUpload/Features.PhotoUpload.csproj", "Features/Features.PhotoUpload/"]
|
COPY ["Features/Features.PhotoUpload/Features.PhotoUpload.csproj", "Features/Features.PhotoUpload/"]
|
||||||
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
||||||
COPY ["Features/Features.UserManagement/Features.UserManagement.csproj", "Features/Features.UserManagement/"]
|
COPY ["Features/Features.Users/Features.Users.csproj", "Features/Features.Users/"]
|
||||||
COPY ["Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj", "Features/Features.UserManagement.Tests/"]
|
COPY ["Features/Features.Users.Tests/Features.Users.Tests.csproj", "Features/Features.Users.Tests/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj", "Infrastructure/Infrastructure.Configuration/"]
|
COPY ["Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj", "Infrastructure/Infrastructure.Configuration/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ using Features.Emails.DependencyInjection;
|
|||||||
using Features.Emails.Services;
|
using Features.Emails.Services;
|
||||||
using Features.ImageUploads.Commands.UploadPhoto;
|
using Features.ImageUploads.Commands.UploadPhoto;
|
||||||
using Features.ImageUploads.DependencyInjection;
|
using Features.ImageUploads.DependencyInjection;
|
||||||
using Features.UserManagement.Controllers;
|
using Features.Users.Controllers;
|
||||||
using Features.UserManagement.DependencyInjection;
|
using Features.Users.DependencyInjection;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.AspNetCore;
|
using FluentValidation.AspNetCore;
|
||||||
using Infrastructure.FileUpload;
|
using Infrastructure.FileUpload;
|
||||||
@@ -25,12 +25,8 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
// Add current controllers to the exception filter
|
// Add current controllers to the exception filter
|
||||||
builder
|
builder
|
||||||
.Services.AddControllers(options =>
|
.Services.AddControllers(options => { options.Filters.Add<GlobalExceptionFilter>(); })
|
||||||
{
|
|
||||||
options.Filters.Add<GlobalExceptionFilter>();
|
|
||||||
})
|
|
||||||
.AddApplicationPart(typeof(BreweryController).Assembly)
|
.AddApplicationPart(typeof(BreweryController).Assembly)
|
||||||
.AddApplicationPart(typeof(UserController).Assembly)
|
|
||||||
.AddApplicationPart(typeof(AuthController).Assembly);
|
.AddApplicationPart(typeof(AuthController).Assembly);
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
@@ -69,7 +65,6 @@ builder.Services.AddOpenApi();
|
|||||||
|
|
||||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(BreweryController).Assembly);
|
builder.Services.AddValidatorsFromAssembly(typeof(BreweryController).Assembly);
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(UserController).Assembly);
|
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(AuthController).Assembly);
|
builder.Services.AddValidatorsFromAssembly(typeof(AuthController).Assembly);
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
builder.Services.AddValidatorsFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
||||||
builder.Services.AddFluentValidationAutoValidation();
|
builder.Services.AddFluentValidationAutoValidation();
|
||||||
@@ -80,7 +75,6 @@ builder.Services.AddMediatR(cfg =>
|
|||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssemblyContaining<Program>();
|
cfg.RegisterServicesFromAssemblyContaining<Program>();
|
||||||
cfg.RegisterServicesFromAssembly(typeof(BreweryController).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(BreweryController).Assembly);
|
||||||
cfg.RegisterServicesFromAssembly(typeof(UserController).Assembly);
|
|
||||||
cfg.RegisterServicesFromAssembly(typeof(AuthController).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(AuthController).Assembly);
|
||||||
cfg.RegisterServicesFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
||||||
cfg.RegisterServicesFromAssembly(typeof(IEmailDispatcher).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(IEmailDispatcher).Assembly);
|
||||||
@@ -98,7 +92,6 @@ if (!builder.Environment.IsProduction())
|
|||||||
builder.Services.AddDatabaseConnection();
|
builder.Services.AddDatabaseConnection();
|
||||||
|
|
||||||
builder.Services.AddFeaturesBreweries();
|
builder.Services.AddFeaturesBreweries();
|
||||||
builder.Services.AddFeaturesUserManagement();
|
|
||||||
builder.Services.AddFeaturesAuth();
|
builder.Services.AddFeaturesAuth();
|
||||||
builder.Services.AddFeaturesEmails();
|
builder.Services.AddFeaturesEmails();
|
||||||
builder.Services.AddFeaturesPhotoUpload();
|
builder.Services.AddFeaturesPhotoUpload();
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ COPY ["Database/Database.Migrations/Database.Migrations.csproj", "Database/Datab
|
|||||||
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
||||||
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
||||||
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"]
|
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"]
|
||||||
COPY ["Features/Features.Auth/Features.Auth.csproj", "Features/Features.Auth/"]
|
|
||||||
COPY ["Features/Features.Auth.Tests/Features.Auth.Tests.csproj", "Features/Features.Auth.Tests/"]
|
|
||||||
COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Features.Breweries/"]
|
COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Features.Breweries/"]
|
||||||
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
||||||
COPY ["Features/Features.Emails/Features.Emails.csproj", "Features/Features.Emails/"]
|
COPY ["Features/Features.Emails/Features.Emails.csproj", "Features/Features.Emails/"]
|
||||||
@@ -22,8 +20,8 @@ COPY ["Features/Features.Locations/Features.Locations.csproj", "Features/Feature
|
|||||||
COPY ["Features/Features.Locations.Tests/Features.Locations.Tests.csproj", "Features/Features.Locations.Tests/"]
|
COPY ["Features/Features.Locations.Tests/Features.Locations.Tests.csproj", "Features/Features.Locations.Tests/"]
|
||||||
COPY ["Features/Features.PhotoUpload/Features.PhotoUpload.csproj", "Features/Features.PhotoUpload/"]
|
COPY ["Features/Features.PhotoUpload/Features.PhotoUpload.csproj", "Features/Features.PhotoUpload/"]
|
||||||
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
||||||
COPY ["Features/Features.UserManagement/Features.UserManagement.csproj", "Features/Features.UserManagement/"]
|
COPY ["Features/Features.Users/Features.Users.csproj", "Features/Features.Users/"]
|
||||||
COPY ["Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj", "Features/Features.UserManagement.Tests/"]
|
COPY ["Features/Features.Users.Tests/Features.Users.Tests.csproj", "Features/Features.Users.Tests/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj", "Infrastructure/Infrastructure.Configuration/"]
|
COPY ["Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj", "Infrastructure/Infrastructure.Configuration/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
<Project Path="Domain/Domain.Entities/Domain.Entities.csproj" />
|
<Project Path="Domain/Domain.Entities/Domain.Entities.csproj" />
|
||||||
<Project Path="Domain/Domain.Exceptions/Domain.Exceptions.csproj" />
|
<Project Path="Domain/Domain.Exceptions/Domain.Exceptions.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/Features/Users/">
|
||||||
|
<Project Path="Features\Features.Users.Tests\Features.Users.Tests.csproj" />
|
||||||
|
<Project Path="Features\Features.Users\Features.Users.csproj" />
|
||||||
|
</Folder>
|
||||||
<Folder Name="/Infrastructure/">
|
<Folder Name="/Infrastructure/">
|
||||||
<Project Path="Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj" />
|
||||||
<Project Path="Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj" />
|
||||||
@@ -20,20 +24,29 @@
|
|||||||
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
||||||
<Project Path="Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj" />
|
<Project Path="Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Features/">
|
<Folder Name="/Infrastructure/Email/">
|
||||||
|
<Project Path="Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj" />
|
||||||
|
<Project Path="Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Features/" />
|
||||||
|
<Folder Name="/Features/Breweries/">
|
||||||
<Project Path="Features/Features.Breweries/Features.Breweries.csproj" />
|
<Project Path="Features/Features.Breweries/Features.Breweries.csproj" />
|
||||||
<Project Path="Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj" />
|
<Project Path="Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj" />
|
||||||
<Project Path="Features/Features.UserManagement/Features.UserManagement.csproj" />
|
</Folder>
|
||||||
<Project Path="Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj" />
|
<Folder Name="/Features/Emails/">
|
||||||
<Project Path="Features/Features.Auth/Features.Auth.csproj" />
|
|
||||||
<Project Path="Features/Features.Auth.Tests/Features.Auth.Tests.csproj" />
|
|
||||||
<Project Path="Features/Features.Emails/Features.Emails.csproj" />
|
<Project Path="Features/Features.Emails/Features.Emails.csproj" />
|
||||||
<Project Path="Features/Features.Emails.Tests/Features.Emails.Tests.csproj" />
|
<Project Path="Features/Features.Emails.Tests/Features.Emails.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Features/Locations/">
|
||||||
<Project Path="Features/Features.Locations/Features.Locations.csproj" />
|
<Project Path="Features/Features.Locations/Features.Locations.csproj" />
|
||||||
<Project Path="Features/Features.Locations.Tests/Features.Locations.Tests.csproj" />
|
<Project Path="Features/Features.Locations.Tests/Features.Locations.Tests.csproj" />
|
||||||
<Project Path="Features\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
<Project Path="Features\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
||||||
<Project Path="Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj" />
|
<Project Path="Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/Features/PhotoUpload/">
|
||||||
|
<Project Path="Features/Features.PhotoUpload/Features.PhotoUpload.csproj" />
|
||||||
|
<Project Path="Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
<Folder Name="/Shared/">
|
<Folder Name="/Shared/">
|
||||||
<Project Path="Shared/Shared.Contracts/Shared.Contracts.csproj" />
|
<Project Path="Shared/Shared.Contracts/Shared.Contracts.csproj" />
|
||||||
<Project Path="Shared/Shared.Application/Shared.Application.csproj" />
|
<Project Path="Shared/Shared.Application/Shared.Application.csproj" />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Users\Features.Users.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Shared.Application.Emails;
|
|||||||
namespace Features.Emails.Commands.SendRegistrationEmail;
|
namespace Features.Emails.Commands.SendRegistrationEmail;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles <see cref="SendRegistrationEmailCommand" />, the cross-slice command sent by Features.Auth
|
/// Handles <see cref="SendRegistrationEmailCommand" />, the cross-slice command sent by Features.Users
|
||||||
/// after a new user registers.
|
/// after a new user registers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SendRegistrationEmailHandler(IEmailDispatcher emailDispatcher)
|
public class SendRegistrationEmailHandler(IEmailDispatcher emailDispatcher)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Shared.Application.Emails;
|
|||||||
namespace Features.Emails.Commands.SendResendConfirmationEmail;
|
namespace Features.Emails.Commands.SendResendConfirmationEmail;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles <see cref="SendResendConfirmationEmailCommand" />, the cross-slice command sent by Features.Auth
|
/// Handles <see cref="SendResendConfirmationEmailCommand" />, the cross-slice command sent by Features.Users
|
||||||
/// when a user requests a fresh confirmation link.
|
/// when a user requests a fresh confirmation link.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SendResendConfirmationEmailHandler(IEmailDispatcher emailDispatcher)
|
public class SendResendConfirmationEmailHandler(IEmailDispatcher emailDispatcher)
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
using Features.UserManagement.Commands.UpdateUser;
|
|
||||||
using Features.UserManagement.Repository;
|
|
||||||
using Moq;
|
|
||||||
|
|
||||||
namespace Features.UserManagement.Tests.Commands;
|
|
||||||
|
|
||||||
public class UpdateUserHandlerTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_DelegatesToRepository()
|
|
||||||
{
|
|
||||||
Mock<IUserAccountRepository> repoMock = new();
|
|
||||||
UpdateUserHandler handler = new(repoMock.Object);
|
|
||||||
UserAccount user = new() { UserAccountId = Guid.NewGuid() };
|
|
||||||
repoMock.Setup(r => r.UpdateAsync(user)).Returns(Task.CompletedTask);
|
|
||||||
|
|
||||||
await handler.Handle(new UpdateUserCommand(user), CancellationToken.None);
|
|
||||||
|
|
||||||
repoMock.Verify(r => r.UpdateAsync(user), Times.Once);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<RootNamespace>Features.UserManagement.Tests</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
|
||||||
<PackageReference Include="FluentAssertions" Version="7.2.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Features.UserManagement\Features.UserManagement.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Features.UserManagement.Commands.UpdateUser;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an existing user account. Not currently exposed via any HTTP route, carried forward
|
|
||||||
/// from <c>IUserService.UpdateAsync</c> as-is.
|
|
||||||
/// </summary>
|
|
||||||
public record UpdateUserCommand(UserAccount UserAccount) : IRequest;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using Features.UserManagement.Repository;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Features.UserManagement.Commands.UpdateUser;
|
|
||||||
|
|
||||||
public class UpdateUserHandler(IUserAccountRepository repository)
|
|
||||||
: IRequestHandler<UpdateUserCommand>
|
|
||||||
{
|
|
||||||
public Task Handle(UpdateUserCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return repository.UpdateAsync(request.UserAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using Features.UserManagement.Repository;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Features.UserManagement.DependencyInjection;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers the services used by the Features.UserManagement slice.
|
|
||||||
/// </summary>
|
|
||||||
public static class FeaturesUserManagementServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Registers <see cref="IUserAccountRepository" /> and its Dapper-based implementation.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The same <paramref name="services" /> instance, for chaining.</returns>
|
|
||||||
public static IServiceCollection AddFeaturesUserManagement(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddScoped<IUserAccountRepository, UserAccountRepository>();
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<RootNamespace>Features.UserManagement</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
using Domain.Exceptions;
|
|
||||||
|
|
||||||
namespace Features.UserManagement.Repository;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Repository for CRUD operations on user account records.
|
|
||||||
/// </summary>
|
|
||||||
public interface IUserAccountRepository
|
|
||||||
{
|
|
||||||
/// <summary>Returns <c>null</c> if no user account exists with the given ID.</summary>
|
|
||||||
Task<UserAccount?> GetByIdAsync(Guid id);
|
|
||||||
|
|
||||||
/// <summary>Returns user accounts ordered by creation date descending.</summary>
|
|
||||||
/// <param name="limit"><c>null</c> for no limit.</param>
|
|
||||||
/// <param name="offset"><c>null</c> for no offset.</param>
|
|
||||||
Task<IEnumerable<UserAccount>> GetAllAsync(int? limit, int? offset);
|
|
||||||
|
|
||||||
/// <summary>Updates the mutable fields of an existing user account.</summary>
|
|
||||||
/// <param name="userAccount">Must have a valid <c>UserAccountId</c>.</param>
|
|
||||||
/// <exception cref="NotFoundException">Thrown when no user account exists with the given ID.</exception>
|
|
||||||
Task UpdateAsync(UserAccount userAccount);
|
|
||||||
|
|
||||||
/// <summary>Deletes the user account with the given ID.</summary>
|
|
||||||
/// <exception cref="NotFoundException">Thrown when no user account exists with the given ID.</exception>
|
|
||||||
Task DeleteAsync(Guid id);
|
|
||||||
|
|
||||||
/// <summary>Returns <c>null</c> if no user account exists with the given username.</summary>
|
|
||||||
Task<UserAccount?> GetByUsernameAsync(string username);
|
|
||||||
|
|
||||||
/// <summary>Returns <c>null</c> if no user account exists with the given email address.</summary>
|
|
||||||
Task<UserAccount?> GetByEmailAsync(string email);
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
using System.Data.Common;
|
|
||||||
using Dapper;
|
|
||||||
using Database.Connection;
|
|
||||||
using Domain.Entities;
|
|
||||||
using Domain.Exceptions;
|
|
||||||
|
|
||||||
namespace Features.UserManagement.Repository;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dapper-based implementation of <see cref="IUserAccountRepository" />.
|
|
||||||
/// </summary>
|
|
||||||
public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
|
||||||
: DapperRepository(connectionFactory),
|
|
||||||
IUserAccountRepository
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<UserAccount?> GetByIdAsync(Guid id)
|
|
||||||
{
|
|
||||||
await using DbConnection connection = await CreateConnection();
|
|
||||||
return await connection.QueryFirstOrDefaultAsync<UserAccount>(
|
|
||||||
"""
|
|
||||||
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
|
|
||||||
FROM dbo.UserAccount
|
|
||||||
WHERE UserAccountID = @UserAccountId
|
|
||||||
""",
|
|
||||||
new { UserAccountId = id }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
/// <remarks>
|
|
||||||
/// Always applies pagination via <c>OFFSET</c>/<c>FETCH</c>, ordered by creation date descending.
|
|
||||||
/// A <c>null</c> <paramref name="limit" />/<paramref name="offset" /> is treated as "no limit"/"no offset".
|
|
||||||
/// </remarks>
|
|
||||||
public async Task<IEnumerable<UserAccount>> GetAllAsync(int? limit, int? offset)
|
|
||||||
{
|
|
||||||
await using DbConnection connection = await CreateConnection();
|
|
||||||
return await connection.QueryAsync<UserAccount>(
|
|
||||||
"""
|
|
||||||
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
|
|
||||||
FROM dbo.UserAccount
|
|
||||||
ORDER BY CreatedAt DESC
|
|
||||||
OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY
|
|
||||||
""",
|
|
||||||
new { Offset = offset ?? 0, Limit = limit ?? int.MaxValue }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task UpdateAsync(UserAccount userAccount)
|
|
||||||
{
|
|
||||||
await using DbConnection connection = await CreateConnection();
|
|
||||||
int rows = await connection.ExecuteAsync(
|
|
||||||
"""
|
|
||||||
UPDATE dbo.UserAccount
|
|
||||||
SET Username = @Username, FirstName = @FirstName, LastName = @LastName,
|
|
||||||
DateOfBirth = @DateOfBirth, Email = @Email
|
|
||||||
WHERE UserAccountId = @UserAccountId
|
|
||||||
""",
|
|
||||||
new
|
|
||||||
{
|
|
||||||
userAccount.UserAccountId,
|
|
||||||
userAccount.Username,
|
|
||||||
userAccount.FirstName,
|
|
||||||
userAccount.LastName,
|
|
||||||
userAccount.DateOfBirth,
|
|
||||||
userAccount.Email,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rows == 0)
|
|
||||||
throw new NotFoundException("User account not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task DeleteAsync(Guid id)
|
|
||||||
{
|
|
||||||
await using DbConnection connection = await CreateConnection();
|
|
||||||
int rows = await connection.ExecuteAsync(
|
|
||||||
"""
|
|
||||||
DELETE FROM dbo.UserAccount
|
|
||||||
WHERE UserAccountId = @UserAccountId
|
|
||||||
""",
|
|
||||||
new { UserAccountId = id }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rows == 0)
|
|
||||||
throw new NotFoundException("User account not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<UserAccount?> GetByUsernameAsync(string username)
|
|
||||||
{
|
|
||||||
await using DbConnection connection = await CreateConnection();
|
|
||||||
return await connection.QueryFirstOrDefaultAsync<UserAccount>(
|
|
||||||
"""
|
|
||||||
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
|
|
||||||
FROM dbo.UserAccount
|
|
||||||
WHERE Username = @Username
|
|
||||||
""",
|
|
||||||
new { Username = username }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<UserAccount?> GetByEmailAsync(string email)
|
|
||||||
{
|
|
||||||
await using DbConnection connection = await CreateConnection();
|
|
||||||
return await connection.QueryFirstOrDefaultAsync<UserAccount>(
|
|
||||||
"""
|
|
||||||
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
|
|
||||||
FROM dbo.UserAccount
|
|
||||||
WHERE Email = @Email
|
|
||||||
""",
|
|
||||||
new { Email = email }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Features.Auth\Features.Auth.csproj" />
|
<ProjectReference Include="..\Features.Users\Features.Users.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Features.UserManagement.Queries.GetAllUsers;
|
using Features.Auth.Queries.GetAllUsers;
|
||||||
using Features.UserManagement.Repository;
|
using Features.Auth.Repository;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
namespace Features.UserManagement.Tests.Queries;
|
namespace Features.Auth.Tests.Queries;
|
||||||
|
|
||||||
public class GetAllUsersHandlerTests
|
public class GetAllUsersHandlerTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_PassesLimitAndOffset_ToRepository()
|
public async Task Handle_PassesLimitAndOffset_ToRepository()
|
||||||
{
|
{
|
||||||
Mock<IUserAccountRepository> repoMock = new();
|
Mock<IUserListRepository> repoMock = new();
|
||||||
GetAllUsersHandler handler = new(repoMock.Object);
|
GetAllUsersHandler handler = new(repoMock.Object);
|
||||||
repoMock.Setup(r => r.GetAllAsync(10, 5)).ReturnsAsync(Array.Empty<UserAccount>());
|
repoMock.Setup(r => r.GetAllAsync(10, 5)).ReturnsAsync(Array.Empty<UserAccount>());
|
||||||
|
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.UserManagement.Queries.GetUserById;
|
using Features.Auth.Queries.GetUserById;
|
||||||
using Features.UserManagement.Repository;
|
using Features.Auth.Repository;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
namespace Features.UserManagement.Tests.Queries;
|
namespace Features.Auth.Tests.Queries;
|
||||||
|
|
||||||
public class GetUserByIdHandlerTests
|
public class GetUserByIdHandlerTests
|
||||||
{
|
{
|
||||||
private readonly GetUserByIdHandler _handler;
|
private readonly GetUserByIdHandler _handler;
|
||||||
private readonly Mock<IUserAccountRepository> _repoMock = new();
|
private readonly Mock<IUserListRepository> _repoMock = new();
|
||||||
|
|
||||||
public GetUserByIdHandlerTests()
|
public GetUserByIdHandlerTests()
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,7 @@ namespace Features.Auth.Commands.UpdateEmail;
|
|||||||
/// Handles <see cref="UpdateEmailCommand" /> via <see cref="UserManager{TUser}" />.
|
/// Handles <see cref="UpdateEmailCommand" /> via <see cref="UserManager{TUser}" />.
|
||||||
/// <see cref="UserManager{TUser}.SetEmailAsync" /> also resets <c>EmailConfirmed</c> to
|
/// <see cref="UserManager{TUser}.SetEmailAsync" /> also resets <c>EmailConfirmed</c> to
|
||||||
/// <see langword="false" />, so the user must re-confirm the new address (see
|
/// <see langword="false" />, so the user must re-confirm the new address (see
|
||||||
/// <c>Features.Auth.Commands.ResendConfirmationEmail</c>).
|
/// <c>Features.Users.Commands.ResendConfirmationEmail</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateEmailHandler(UserManager<ApplicationUser> userManager)
|
public class UpdateEmailHandler(UserManager<ApplicationUser> userManager)
|
||||||
: IRequestHandler<UpdateEmailCommand, UpdateEmailPayload>
|
: IRequestHandler<UpdateEmailCommand, UpdateEmailPayload>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.UserManagement.Queries.GetAllUsers;
|
using Features.Auth.Queries.GetAllUsers;
|
||||||
using Features.UserManagement.Queries.GetUserById;
|
using Features.Auth.Queries.GetUserById;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Features.UserManagement.Controllers;
|
namespace Features.Auth.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exposes read endpoints for user accounts.
|
/// Exposes read endpoints for user accounts.
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Features.Auth.Identity;
|
using Features.Auth.Identity;
|
||||||
|
using Features.Auth.Repository;
|
||||||
using Features.Auth.Services;
|
using Features.Auth.Services;
|
||||||
using Infrastructure.PasswordHashing;
|
using Infrastructure.PasswordHashing;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@@ -6,17 +7,19 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Features.Auth.DependencyInjection;
|
namespace Features.Auth.DependencyInjection;
|
||||||
|
|
||||||
/// <summary>Registers the services required by Features.Auth with the dependency injection container.</summary>
|
/// <summary>Registers the services required by Features.Users with the dependency injection container.</summary>
|
||||||
public static class FeaturesAuthServiceCollectionExtensions
|
public static class FeaturesAuthServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds <see cref="ITokenService" /> and ASP.NET Core Identity (<see cref="UserManager{TUser}" />
|
/// Adds <see cref="ITokenService" />, <see cref="IUserListRepository" />, and ASP.NET Core
|
||||||
/// backed by <see cref="DapperUserStore" /> and <see cref="Argon2PasswordHasher" />) as scoped services.
|
/// Identity (<see cref="UserManager{TUser}" /> backed by <see cref="DapperUserStore" /> and
|
||||||
|
/// <see cref="Argon2PasswordHasher" />) as scoped services.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IServiceCollection AddFeaturesAuth(this IServiceCollection services)
|
public static IServiceCollection AddFeaturesAuth(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<ITokenService, TokenService>();
|
services.AddScoped<ITokenService, TokenService>();
|
||||||
services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();
|
services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();
|
||||||
|
services.AddScoped<IUserListRepository, UserListRepository>();
|
||||||
|
|
||||||
// AddDefaultTokenProviders() is deliberately not called: it registers
|
// AddDefaultTokenProviders() is deliberately not called: it registers
|
||||||
// DataProtectorTokenProvider<TUser>, which UserManager's constructor resolves eagerly and which
|
// DataProtectorTokenProvider<TUser>, which UserManager's constructor resolves eagerly and which
|
||||||
@@ -18,11 +18,8 @@
|
|||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
|
<ProjectReference Include="..\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Queries\" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -2,7 +2,7 @@ namespace Features.Auth.Identity;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The user type driving <see cref="Microsoft.AspNetCore.Identity.UserManager{TUser}" /> for
|
/// The user type driving <see cref="Microsoft.AspNetCore.Identity.UserManager{TUser}" /> for
|
||||||
/// Features.Auth. A plain POCO (not <c>IdentityUser<TKey></c>) backed by <see cref="DapperUserStore" />
|
/// Features.Users. A plain POCO (not <c>IdentityUser<TKey></c>) backed by <see cref="DapperUserStore" />
|
||||||
/// rather than EF Core, matching the rest of the solution's Dapper-only persistence.
|
/// rather than EF Core, matching the rest of the solution's Dapper-only persistence.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ApplicationUser
|
public sealed class ApplicationUser
|
||||||
@@ -186,8 +186,9 @@ public sealed class DapperUserStore(ISqlConnectionFactory connectionFactory)
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// A plain <c>DELETE</c> against <c>dbo.UserAccount</c>: its foreign keys from
|
/// A plain <c>DELETE</c> against <c>dbo.UserAccount</c>: its foreign keys from
|
||||||
/// <c>dbo.UserCredential</c>, <c>dbo.UserVerification</c>, and <c>dbo.UserAvatar</c> cascade, so no
|
/// <c>dbo.UserCredential</c>, <c>dbo.UserVerification</c>, and <c>dbo.UserProfile</c> cascade
|
||||||
/// manual child cleanup is needed. Other tables (posts, comments, photos, follows) reference the
|
/// (<c>dbo.UserAvatar</c> cascades transitively through <c>dbo.UserProfile</c>), so no manual
|
||||||
|
/// child cleanup is needed. Other tables (posts, comments, photos, follows) reference the
|
||||||
/// account with <c>ON DELETE NO ACTION</c> by design; deleting an account that still has any of
|
/// account with <c>ON DELETE NO ACTION</c> by design; deleting an account that still has any of
|
||||||
/// those fails with a foreign key violation, which is surfaced as a failed
|
/// those fails with a foreign key violation, which is surfaced as a failed
|
||||||
/// <see cref="IdentityResult" /> rather than an unhandled <see cref="SqlException" />.
|
/// <see cref="IdentityResult" /> rather than an unhandled <see cref="SqlException" />.
|
||||||
@@ -5,7 +5,7 @@ namespace Features.Auth.Identity;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the persistence-agnostic <see cref="UserAccount" /> domain entity into the
|
/// Converts the persistence-agnostic <see cref="UserAccount" /> domain entity into the
|
||||||
/// <see cref="ApplicationUser" /> shape required by <see cref="Microsoft.AspNetCore.Identity.UserManager{TUser}" />.
|
/// <see cref="ApplicationUser" /> shape required by <see cref="Microsoft.AspNetCore.Identity.UserManager{TUser}" />.
|
||||||
/// Kept here, in Features.Auth, rather than on <see cref="UserAccount" /> itself so the domain layer has
|
/// Kept here, in Features.Users, rather than on <see cref="UserAccount" /> itself so the domain layer has
|
||||||
/// no dependency on ASP.NET Core Identity.
|
/// no dependency on ASP.NET Core Identity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UserAccountExtensions
|
public static class UserAccountExtensions
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Features.UserManagement.Repository;
|
using Features.Auth.Repository;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Features.UserManagement.Queries.GetAllUsers;
|
namespace Features.Auth.Queries.GetAllUsers;
|
||||||
|
|
||||||
public class GetAllUsersHandler(IUserAccountRepository repository)
|
public class GetAllUsersHandler(IUserListRepository repository)
|
||||||
: IRequestHandler<GetAllUsersQuery, IEnumerable<UserAccount>>
|
: IRequestHandler<GetAllUsersQuery, IEnumerable<UserAccount>>
|
||||||
{
|
{
|
||||||
public Task<IEnumerable<UserAccount>> Handle(
|
public Task<IEnumerable<UserAccount>> Handle(
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Features.UserManagement.Queries.GetAllUsers;
|
namespace Features.Auth.Queries.GetAllUsers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Requests a page of user accounts, ordered by creation date descending.
|
/// Requests a page of user accounts, ordered by creation date descending.
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.UserManagement.Repository;
|
using Features.Auth.Repository;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Features.UserManagement.Queries.GetUserById;
|
namespace Features.Auth.Queries.GetUserById;
|
||||||
|
|
||||||
public class GetUserByIdHandler(IUserAccountRepository repository)
|
public class GetUserByIdHandler(IUserListRepository repository)
|
||||||
: IRequestHandler<GetUserByIdQuery, UserAccount>
|
: IRequestHandler<GetUserByIdQuery, UserAccount>
|
||||||
{
|
{
|
||||||
/// <exception cref="NotFoundException">Thrown when no user account exists with the given ID.</exception>
|
/// <exception cref="NotFoundException">Thrown when no user account exists with the given ID.</exception>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Features.UserManagement.Queries.GetUserById;
|
namespace Features.Auth.Queries.GetUserById;
|
||||||
|
|
||||||
public record GetUserByIdQuery(Guid UserAccountId) : IRequest<UserAccount>;
|
public record GetUserByIdQuery(Guid UserAccountId) : IRequest<UserAccount>;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
|
||||||
|
namespace Features.Auth.Repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read access for user account listing. Everything else on user accounts (lookup by
|
||||||
|
/// username/email, create/update/delete) already goes through
|
||||||
|
/// <see cref="Microsoft.AspNetCore.Identity.UserManager{TUser}" />; this repository only covers
|
||||||
|
/// what <c>IUserStore</c> has no equivalent for: fetching by ID with full audit fields, and
|
||||||
|
/// paginated listing.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserListRepository
|
||||||
|
{
|
||||||
|
/// <summary>Returns <c>null</c> if no user account exists with the given ID.</summary>
|
||||||
|
Task<UserAccount?> GetByIdAsync(Guid id);
|
||||||
|
|
||||||
|
/// <summary>Returns user accounts ordered by creation date descending.</summary>
|
||||||
|
/// <param name="limit"><c>null</c> for no limit.</param>
|
||||||
|
/// <param name="offset"><c>null</c> for no offset.</param>
|
||||||
|
Task<IEnumerable<UserAccount>> GetAllAsync(int? limit, int? offset);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
using Dapper;
|
||||||
|
using Database.Connection;
|
||||||
|
using Domain.Entities;
|
||||||
|
|
||||||
|
namespace Features.Auth.Repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dapper-based implementation of <see cref="IUserListRepository" />.
|
||||||
|
/// </summary>
|
||||||
|
public class UserListRepository(ISqlConnectionFactory connectionFactory)
|
||||||
|
: DapperRepository(connectionFactory),
|
||||||
|
IUserListRepository
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<UserAccount?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
await using DbConnection connection = await CreateConnection();
|
||||||
|
return await connection.QueryFirstOrDefaultAsync<UserAccount>(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
UserAccountID,
|
||||||
|
Username,
|
||||||
|
FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
|
||||||
|
FROM dbo.UserAccount
|
||||||
|
WHERE UserAccountID = @UserAccountId
|
||||||
|
""",
|
||||||
|
new { UserAccountId = id }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <remarks>
|
||||||
|
/// Always applies pagination via <c>OFFSET</c>/<c>FETCH</c>, ordered by creation date descending.
|
||||||
|
/// A <c>null</c> <paramref name="limit" />/<paramref name="offset" /> is treated as "no limit"/"no offset".
|
||||||
|
/// </remarks>
|
||||||
|
public async Task<IEnumerable<UserAccount>> GetAllAsync(int? limit, int? offset)
|
||||||
|
{
|
||||||
|
await using DbConnection connection = await CreateConnection();
|
||||||
|
return await connection.QueryAsync<UserAccount>(
|
||||||
|
"""
|
||||||
|
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
|
||||||
|
FROM dbo.UserAccount
|
||||||
|
ORDER BY CreatedAt DESC
|
||||||
|
OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY
|
||||||
|
""",
|
||||||
|
new { Offset = offset ?? 0, Limit = limit ?? int.MaxValue }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using MediatR;
|
|||||||
namespace Shared.Application.Emails;
|
namespace Shared.Application.Emails;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cross-slice command sent by Features.Auth and handled by Features.Emails to dispatch a
|
/// Cross-slice command sent by Features.Users and handled by Features.Emails to dispatch a
|
||||||
/// registration confirmation email, without either slice taking a project reference on the other.
|
/// registration confirmation email, without either slice taking a project reference on the other.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record SendRegistrationEmailCommand(string FirstName, string Email, string ConfirmationToken)
|
public record SendRegistrationEmailCommand(string FirstName, string Email, string ConfirmationToken)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using MediatR;
|
|||||||
namespace Shared.Application.Emails;
|
namespace Shared.Application.Emails;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cross-slice command sent by Features.Auth and handled by Features.Emails to dispatch a
|
/// Cross-slice command sent by Features.Users and handled by Features.Emails to dispatch a
|
||||||
/// fresh confirmation-link email, without either slice taking a project reference on the other.
|
/// fresh confirmation-link email, without either slice taking a project reference on the other.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record SendResendConfirmationEmailCommand(
|
public record SendResendConfirmationEmailCommand(
|
||||||
|
|||||||
Reference in New Issue
Block a user