From 84457413b2dc525e5fac94149b7e80c2f60cf959 Mon Sep 17 00:00:00 2001 From: john Date: Mon, 16 Jun 2025 21:11:40 +0200 Subject: [PATCH] refactor --- Femto.Api/Controllers/Auth/AuthController.cs | 30 ++--- ...neBehaviour.cs => DDDPipelineBehaviour.cs} | 4 +- .../Infrastructure/DomainServiceExtensions.cs | 2 +- Femto.Modules.Auth/Application/AuthStartup.cs | 7 +- .../CreateSignupCodeCommand.cs | 5 - .../CreateSignupCodeCommandHandler.cs | 15 --- .../Deauthenticate/DeauthenticateCommand.cs | 6 - .../DeauthenticateCommandHandler.cs | 12 -- .../GetSignupCodesQuery.cs | 6 - .../GetSignupCodesQueryHandler.cs | 55 -------- .../GetUserInfo/GetUserInfoCommand.cs | 6 - .../GetUserInfo/GetUserInfoCommandHandler.cs | 27 ---- .../Interface/Register/RegisterCommand.cs | 6 - .../Register/RegisterCommandHandler.cs | 43 ------ .../Application/Services/AuthModule.cs | 20 --- .../Application/Services/AuthService.cs | 124 +++++++++++++++++- .../Application/Services/IAuthModule.cs | 10 -- .../Application/Services/IAuthService.cs | 26 +++- Femto.Modules.Auth/Data/AuthContext.cs | 43 ++++++ .../SaveChangesPipelineBehaviour.cs | 23 ++++ 20 files changed, 224 insertions(+), 246 deletions(-) rename Femto.Common/Infrastructure/{SaveChangesPipelineBehaviour.cs => DDDPipelineBehaviour.cs} (88%) delete mode 100644 Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommand.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommandHandler.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommand.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommandHandler.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs delete mode 100644 Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs delete mode 100644 Femto.Modules.Auth/Application/Services/AuthModule.cs delete mode 100644 Femto.Modules.Auth/Application/Services/IAuthModule.cs create mode 100644 Femto.Modules.Auth/Infrastructure/SaveChangesPipelineBehaviour.cs diff --git a/Femto.Api/Controllers/Auth/AuthController.cs b/Femto.Api/Controllers/Auth/AuthController.cs index e45e73c..a91d906 100644 --- a/Femto.Api/Controllers/Auth/AuthController.cs +++ b/Femto.Api/Controllers/Auth/AuthController.cs @@ -1,24 +1,16 @@ -using Femto.Api.Auth; using Femto.Api.Sessions; using Femto.Common; -using Femto.Modules.Auth.Application.Interface.CreateSignupCode; -using Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; -using Femto.Modules.Auth.Application.Interface.Register; using Femto.Modules.Auth.Application.Services; using Femto.Modules.Auth.Contracts; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace Femto.Api.Controllers.Auth; [ApiController] [Route("auth")] public class AuthController( - IAuthModule authModule, - IOptions cookieSettings, ICurrentUserContext currentUserContext, - ILogger logger, IAuthService authService ) : ControllerBase { @@ -28,17 +20,17 @@ public class AuthController( CancellationToken cancellationToken ) { - var user = await authService.GetUserWithCredentials( + var result = await authService.GetUserWithCredentials( request.Username, request.Password, cancellationToken ); - if (user is null) + if (result is null) return this.BadRequest(); - var session = await authService.CreateStrongSession(user.Id); - + var (user, session) = result; + HttpContext.SetSession(session, user); return new LoginResponse(user.Id, user.Username, user.Roles.Any(r => r == Role.SuperUser)); @@ -47,13 +39,10 @@ public class AuthController( [HttpPost("register")] public async Task> Register([FromBody] RegisterRequest request) { - var user = await authModule.Command( - new RegisterCommand(request.Username, request.Password, request.SignupCode) - ); - - var session = await authService.CreateStrongSession(user.Id); + var (user, session) = await authService.CreateUserWithCredentials(request.Username, request.Password, request.SignupCode); HttpContext.SetSession(session, user); + return new RegisterResponse( user.Id, user.Username, @@ -106,10 +95,7 @@ public class AuthController( CancellationToken cancellationToken ) { - await authModule.Command( - new CreateSignupCodeCommand(request.Code, request.Email, request.Name), - cancellationToken - ); + await authService.AddSignupCode(request.Code, request.Name, cancellationToken); return Ok(new { }); } @@ -120,7 +106,7 @@ public class AuthController( CancellationToken cancellationToken ) { - var codes = await authModule.Query(new GetSignupCodesQuery(), cancellationToken); + var codes = await authService.GetSignupCodes(cancellationToken); return new ListSignupCodesResult( codes.Select(c => new SignupCodeDto( diff --git a/Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs b/Femto.Common/Infrastructure/DDDPipelineBehaviour.cs similarity index 88% rename from Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs rename to Femto.Common/Infrastructure/DDDPipelineBehaviour.cs index b86a7e4..e5f338f 100644 --- a/Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs +++ b/Femto.Common/Infrastructure/DDDPipelineBehaviour.cs @@ -5,10 +5,10 @@ using Microsoft.Extensions.Logging; namespace Femto.Common.Infrastructure; -public class SaveChangesPipelineBehaviour( +public class DDDPipelineBehaviour( DbContext context, IPublisher publisher, - ILogger> logger + ILogger> logger ) : IPipelineBehavior where TRequest : notnull { diff --git a/Femto.Common/Infrastructure/DomainServiceExtensions.cs b/Femto.Common/Infrastructure/DomainServiceExtensions.cs index e83469e..9812c93 100644 --- a/Femto.Common/Infrastructure/DomainServiceExtensions.cs +++ b/Femto.Common/Infrastructure/DomainServiceExtensions.cs @@ -12,7 +12,7 @@ public static class DomainServiceExtensions services.AddScoped(s => s.GetRequiredService()); services.AddTransient( typeof(IPipelineBehavior<,>), - typeof(SaveChangesPipelineBehaviour<,>) + typeof(DDDPipelineBehaviour<,>) ); } diff --git a/Femto.Modules.Auth/Application/AuthStartup.cs b/Femto.Modules.Auth/Application/AuthStartup.cs index c78e923..4fb7b22 100644 --- a/Femto.Modules.Auth/Application/AuthStartup.cs +++ b/Femto.Modules.Auth/Application/AuthStartup.cs @@ -41,7 +41,6 @@ public static class AuthStartup } ); - rootContainer.ExposeScopedService(); rootContainer.ExposeScopedService(); rootContainer.AddHostedService(services => new AuthApplication(host)); @@ -85,8 +84,12 @@ public static class AuthStartup services.AddSingleton(publisher); services.AddSingleton(); + + services.AddScoped( + typeof(IPipelineBehavior<,>), + typeof(SaveChangesPipelineBehaviour<,>) + ); - services.AddScoped(); services.AddScoped(); } diff --git a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs deleted file mode 100644 index be24aa9..0000000 --- a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Femto.Common.Domain; - -namespace Femto.Modules.Auth.Application.Interface.CreateSignupCode; - -public record CreateSignupCodeCommand(string Code, string RecipientEmail, string RecipientName): ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs deleted file mode 100644 index cfbb44a..0000000 --- a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Data; -using Femto.Modules.Auth.Models; - -namespace Femto.Modules.Auth.Application.Interface.CreateSignupCode; - -internal class CreateSignupCodeCommandHandler(AuthContext context) : ICommandHandler -{ - public async Task Handle(CreateSignupCodeCommand command, CancellationToken cancellationToken) - { - var code = new SignupCode(command.RecipientEmail, command.RecipientName, command.Code); - - await context.SignupCodes.AddAsync(code, cancellationToken); - } -} diff --git a/Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommand.cs b/Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommand.cs deleted file mode 100644 index 44c346f..0000000 --- a/Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ - -using Femto.Common.Domain; - -namespace Femto.Modules.Auth.Application.Interface.Deauthenticate; - -public record DeauthenticateCommand(Guid UserId, string SessionId, string? RememberMeToken) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommandHandler.cs deleted file mode 100644 index 435718c..0000000 --- a/Femto.Modules.Auth/Application/Interface/Deauthenticate/DeauthenticateCommandHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Data; - -namespace Femto.Modules.Auth.Application.Interface.Deauthenticate; - -internal class DeauthenticateCommandHandler(AuthContext context) : ICommandHandler -{ - public async Task Handle(DeauthenticateCommand request, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs deleted file mode 100644 index 422a09d..0000000 --- a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; - -public record GetSignupCodesQuery: IQuery>; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs deleted file mode 100644 index 201fdce..0000000 --- a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Dapper; -using Femto.Common.Domain; -using Femto.Common.Infrastructure.DbConnection; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; - -public class GetSignupCodesQueryHandler(IDbConnectionFactory connectionFactory) - : IQueryHandler> -{ - public async Task> Handle( - GetSignupCodesQuery request, - CancellationToken cancellationToken - ) - { - using var conn = connectionFactory.GetConnection(); - - // lang=sql - const string sql = """ - SELECT - sc.code as Code, - sc.recipient_email as Email, - sc.recipient_name as Name, - sc.redeeming_user_id as RedeemedByUserId, - u.username as RedeemedByUsername, - sc.expires_at as ExpiresOn - FROM authn.signup_code sc - LEFT JOIN authn.user_identity u ON u.id = sc.redeeming_user_id - ORDER BY sc.created_at DESC - """; - - var result = await conn.QueryAsync(sql); - - return result - .Select(row => new SignupCodeDto( - row.Code, - row.Email, - row.Name, - row.RedeemedByUserId, - row.RedeemedByUsername, - row.ExpiresOn - )) - .ToList(); - } - - private class QueryResultRow - { - public string Code { get; set; } - public string Email { get; set; } - public string Name { get; set; } - public Guid? RedeemedByUserId { get; set; } - public string? RedeemedByUsername { get; set; } - public DateTimeOffset? ExpiresOn { get; set; } - } -} diff --git a/Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommand.cs b/Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommand.cs deleted file mode 100644 index 430b0d3..0000000 --- a/Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.GetUserInfo; - -public record GetUserInfoCommand(Guid ForUser) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommandHandler.cs deleted file mode 100644 index 72c5f20..0000000 --- a/Femto.Modules.Auth/Application/Interface/GetUserInfo/GetUserInfoCommandHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; -using Femto.Modules.Auth.Data; -using Microsoft.EntityFrameworkCore; - -namespace Femto.Modules.Auth.Application.Interface.GetUserInfo; - -internal class GetUserInfoCommandHandler(AuthContext context) - : ICommandHandler -{ - public async Task Handle( - GetUserInfoCommand request, - CancellationToken cancellationToken - ) - { - - var user = await context.Users.SingleOrDefaultAsync( - u => u.Id == request.ForUser, - cancellationToken - ); - - if (user is null) - return null; - - return new UserInfo(user); - } -} diff --git a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs deleted file mode 100644 index 87332cb..0000000 --- a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.Register; - -public record RegisterCommand(string Username, string Password, string SignupCode) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs deleted file mode 100644 index 7bb17be..0000000 --- a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; -using Femto.Modules.Auth.Data; -using Femto.Modules.Auth.Models; -using Microsoft.EntityFrameworkCore; - -namespace Femto.Modules.Auth.Application.Interface.Register; - -internal class RegisterCommandHandler(AuthContext context) - : ICommandHandler -{ - public async Task Handle(RegisterCommand request, CancellationToken cancellationToken) - { - var now = DateTimeOffset.UtcNow; - - var code = await context - .SignupCodes.Where(c => c.Code == request.SignupCode) - .Where(c => c.ExpiresAt == null || c.ExpiresAt > now) - .Where(c => c.RedeemingUserId == null) - .SingleOrDefaultAsync(cancellationToken); - - if (code is null) - throw new DomainError("invalid signup code"); - - var usernameTaken = await context.Users.AnyAsync( - u => u.Username == request.Username, - cancellationToken - ); - - if (usernameTaken) - throw new DomainError("username taken"); - - var user = new UserIdentity(request.Username); - - await context.AddAsync(user, cancellationToken); - - user.SetPassword(request.Password); - - code.Redeem(user.Id); - - return new UserInfo(user); - } -} diff --git a/Femto.Modules.Auth/Application/Services/AuthModule.cs b/Femto.Modules.Auth/Application/Services/AuthModule.cs deleted file mode 100644 index f64d78f..0000000 --- a/Femto.Modules.Auth/Application/Services/AuthModule.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Femto.Common.Domain; -using MediatR; - -namespace Femto.Modules.Auth.Application.Services; - -internal class AuthModule(IMediator mediator) : IAuthModule -{ - public async Task Command(ICommand command, CancellationToken cancellationToken = default) => - await mediator.Send(command, cancellationToken); - - public async Task Command( - ICommand command, - CancellationToken cancellationToken = default - ) => await mediator.Send(command, cancellationToken); - - public async Task Query( - IQuery query, - CancellationToken cancellationToken = default - ) => await mediator.Send(query, cancellationToken); -} diff --git a/Femto.Modules.Auth/Application/Services/AuthService.cs b/Femto.Modules.Auth/Application/Services/AuthService.cs index 1a9f868..0a73d60 100644 --- a/Femto.Modules.Auth/Application/Services/AuthService.cs +++ b/Femto.Modules.Auth/Application/Services/AuthService.cs @@ -1,4 +1,6 @@ +using Dapper; using Femto.Common.Domain; +using Femto.Common.Infrastructure.DbConnection; using Femto.Modules.Auth.Application.Dto; using Femto.Modules.Auth.Data; using Femto.Modules.Auth.Infrastructure; @@ -7,9 +9,13 @@ using Microsoft.EntityFrameworkCore; namespace Femto.Modules.Auth.Application.Services; -internal class AuthService(AuthContext context, SessionStorage storage) : IAuthService +internal class AuthService( + AuthContext context, + SessionStorage storage, + IDbConnectionFactory connectionFactory +) : IAuthService { - public async Task GetUserWithCredentials( + public async Task GetUserWithCredentials( string username, string password, CancellationToken cancellationToken = default @@ -18,14 +24,21 @@ internal class AuthService(AuthContext context, SessionStorage storage) : IAuthS var user = await context .Users.Where(u => u.Username == username) .SingleOrDefaultAsync(cancellationToken); - + if (user is null) return null; if (!user.HasPassword(password)) return null; - - return new UserInfo(user.Id, user.Username, user.Roles.Select(r => r.Role).ToList()); + + var session = new Session(user.Id, true); + + await storage.AddSession(session); + + return new( + new UserInfo(user.Id, user.Username, user.Roles.Select(r => r.Role).ToList()), + session + ); } public Task GetUserWithId(Guid? userId, CancellationToken cancellationToken) @@ -64,6 +77,97 @@ internal class AuthService(AuthContext context, SessionStorage storage) : IAuthS await storage.DeleteSession(sessionId); } + public async Task CreateUserWithCredentials( + string username, + string password, + string signupCode, + CancellationToken cancellationToken = default + ) + { + var now = DateTimeOffset.UtcNow; + + var code = await context + .SignupCodes.Where(c => c.Code == signupCode) + .Where(c => c.ExpiresAt == null || c.ExpiresAt > now) + .Where(c => c.RedeemingUserId == null) + .SingleOrDefaultAsync(cancellationToken); + + if (code is null) + throw new DomainError("invalid signup code"); + + var usernameTaken = await context.Users.AnyAsync( + u => u.Username == username, + cancellationToken + ); + + if (usernameTaken) + throw new DomainError("username taken"); + + var user = new UserIdentity(username); + + await context.AddAsync(user, cancellationToken); + + user.SetPassword(password); + + code.Redeem(user.Id); + + var session = new Session(user.Id, true); + + await storage.AddSession(session); + + await context.SaveChangesAsync(cancellationToken); + + return new(new UserInfo(user), session); + } + + public async Task AddSignupCode( + string code, + string recipientName, + CancellationToken cancellationToken + ) + { + await context.SignupCodes.AddAsync( + new SignupCode("", recipientName, code), + cancellationToken + ); + + await context.SaveChangesAsync(cancellationToken); + } + + public async Task> GetSignupCodes( + CancellationToken cancellationToken = default + ) + { + using var conn = connectionFactory.GetConnection(); + + // lang=sql + const string sql = """ + SELECT + sc.code as Code, + sc.recipient_email as Email, + sc.recipient_name as Name, + sc.redeeming_user_id as RedeemedByUserId, + u.username as RedeemedByUsername, + sc.expires_at as ExpiresOn + FROM authn.signup_code sc + LEFT JOIN authn.user_identity u ON u.id = sc.redeeming_user_id + ORDER BY sc.created_at DESC + """; + + var result = await conn.QueryAsync(sql, cancellationToken); + + return result + .Select(row => new SignupCodeDto( + row.Code, + row.Email, + row.Name, + row.RedeemedByUserId, + row.RedeemedByUsername, + row.ExpiresOn + )) + .ToList(); + } + public async Task CreateLongTermSession(Guid userId, bool isStrong) { throw new NotImplementedException(); @@ -83,4 +187,14 @@ internal class AuthService(AuthContext context, SessionStorage storage) : IAuthS { throw new NotImplementedException(); } + + private class GetSignupCodesQueryResultRow + { + public string Code { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public Guid? RedeemedByUserId { get; set; } + public string? RedeemedByUsername { get; set; } + public DateTimeOffset? ExpiresOn { get; set; } + } } diff --git a/Femto.Modules.Auth/Application/Services/IAuthModule.cs b/Femto.Modules.Auth/Application/Services/IAuthModule.cs deleted file mode 100644 index df34366..0000000 --- a/Femto.Modules.Auth/Application/Services/IAuthModule.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Femto.Common.Domain; - -namespace Femto.Modules.Auth.Application.Services; - -public interface IAuthModule -{ - Task Command(ICommand command, CancellationToken cancellationToken = default); - Task Command(ICommand command, CancellationToken cancellationToken = default); - Task Query(IQuery query, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Services/IAuthService.cs b/Femto.Modules.Auth/Application/Services/IAuthService.cs index 2858053..f07939e 100644 --- a/Femto.Modules.Auth/Application/Services/IAuthService.cs +++ b/Femto.Modules.Auth/Application/Services/IAuthService.cs @@ -11,10 +11,30 @@ namespace Femto.Modules.Auth.Application.Services; /// public interface IAuthService { - public Task GetUserWithCredentials(string username, string password, CancellationToken cancellationToken = default); - public Task GetUserWithId(Guid? userId, CancellationToken cancellationToken = default); + public Task GetUserWithCredentials( + string username, + string password, + CancellationToken cancellationToken = default + ); + public Task GetUserWithId( + Guid? userId, + CancellationToken cancellationToken = default + ); public Task CreateStrongSession(Guid userId); public Task CreateWeakSession(Guid userId); public Task GetSession(string sessionId); public Task DeleteSession(string sessionId); -} \ No newline at end of file + + public Task CreateUserWithCredentials( + string username, + string password, + string signupCode, + CancellationToken cancellationToken = default + ); + + public Task AddSignupCode(string code, string recipientName, CancellationToken cancellationToken = default); + + public Task> GetSignupCodes(CancellationToken cancellationToken = default); +} + +public record UserAndSession(UserInfo User, Session Session); diff --git a/Femto.Modules.Auth/Data/AuthContext.cs b/Femto.Modules.Auth/Data/AuthContext.cs index e4488e4..ac395ba 100644 --- a/Femto.Modules.Auth/Data/AuthContext.cs +++ b/Femto.Modules.Auth/Data/AuthContext.cs @@ -1,6 +1,10 @@ +using Femto.Common.Domain; using Femto.Common.Infrastructure.Outbox; using Femto.Modules.Auth.Models; +using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Logging; namespace Femto.Modules.Auth.Data; @@ -17,4 +21,43 @@ internal class AuthContext(DbContextOptions options) : DbContext(op builder.HasDefaultSchema("authn"); builder.ApplyConfigurationsFromAssembly(typeof(AuthContext).Assembly); } + + public override int SaveChanges() + { + throw new InvalidOperationException("Use SaveChangesAsync instead"); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + await EmitDomainEvents(cancellationToken); + + return await base.SaveChangesAsync(cancellationToken); + } + + private async Task EmitDomainEvents(CancellationToken cancellationToken) + { + var logger = this.GetService>(); + var publisher = this.GetService(); + var domainEvents = this + .ChangeTracker.Entries() + .SelectMany(e => + { + var events = e.Entity.DomainEvents; + e.Entity.ClearDomainEvents(); + return events; + }) + .ToList(); + + logger.LogTrace("loaded {Count} domain events", domainEvents.Count); + + foreach (var domainEvent in domainEvents) + { + logger.LogTrace( + "publishing {Type} domain event {Id}", + domainEvent.GetType().Name, + domainEvent.EventId + ); + await publisher.Publish(domainEvent, cancellationToken); + } + } } \ No newline at end of file diff --git a/Femto.Modules.Auth/Infrastructure/SaveChangesPipelineBehaviour.cs b/Femto.Modules.Auth/Infrastructure/SaveChangesPipelineBehaviour.cs new file mode 100644 index 0000000..cc4f983 --- /dev/null +++ b/Femto.Modules.Auth/Infrastructure/SaveChangesPipelineBehaviour.cs @@ -0,0 +1,23 @@ +using Femto.Modules.Auth.Data; +using MediatR; + +namespace Femto.Modules.Auth.Infrastructure; + +internal class SaveChangesPipelineBehaviour(AuthContext context) + : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) + { + var response = await next(cancellationToken); + + if (context.ChangeTracker.HasChanges()) + await context.SaveChangesAsync(cancellationToken); + + return response; + } +}