From ed9d93cba970531b34644f2997a7afa6a483615d Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Mon, 29 Apr 2024 16:48:42 -0400 Subject: [PATCH] Actor synchronization. (#5) --- .../Accounts/Commands/SignInCommandHandler.cs | 13 ++++-- .../Accounts/Events/UserSignedInEvent.cs | 6 +++ .../Entities/ActorEntity.cs | 25 ++++++++++- .../Handlers/Accounts.cs | 41 +++++++++++++++++++ .../Queriers/ProjectQuerier.cs | 10 ++--- .../Controllers/AccountController.cs | 2 - .../IntegrationTests.cs | 11 +++-- 7 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 backend/src/Logitar.Master.Application/Accounts/Events/UserSignedInEvent.cs create mode 100644 backend/src/Logitar.Master.EntityFrameworkCore/Handlers/Accounts.cs diff --git a/backend/src/Logitar.Master.Application/Accounts/Commands/SignInCommandHandler.cs b/backend/src/Logitar.Master.Application/Accounts/Commands/SignInCommandHandler.cs index ffac9d0..1288b9d 100644 --- a/backend/src/Logitar.Master.Application/Accounts/Commands/SignInCommandHandler.cs +++ b/backend/src/Logitar.Master.Application/Accounts/Commands/SignInCommandHandler.cs @@ -1,4 +1,5 @@ -using Logitar.Master.Contracts.Accounts; +using Logitar.Master.Application.Accounts.Events; +using Logitar.Master.Contracts.Accounts; using Logitar.Portal.Contracts; using Logitar.Portal.Contracts.Messages; using Logitar.Portal.Contracts.Passwords; @@ -19,15 +20,17 @@ internal class SignInCommandHandler : IRequestHandler HandleCredentialsAsync(Credentials crede MultiFactorAuthenticationMode? mfaMode = user.GetMultiFactorAuthenticationMode(); if (mfaMode == MultiFactorAuthenticationMode.None && user.IsProfileCompleted()) { - Session session = await _sessionService.SignInAsync(user, credentials.Password, customAttributes, cancellationToken); // TODO(fpion): create actor + Session session = await _sessionService.SignInAsync(user, credentials.Password, customAttributes, cancellationToken); + await _publisher.Publish(new UserSignedInEvent(session), cancellationToken); return SignInCommandResult.Succeed(session); } else @@ -184,7 +188,8 @@ private async Task EnsureProfileIsCompleted(User user, IEnu return SignInCommandResult.RequireProfileCompletion(token); } - Session session = await _sessionService.CreateAsync(user, customAttributes, cancellationToken); // TODO(fpion): create actor + Session session = await _sessionService.CreateAsync(user, customAttributes, cancellationToken); + await _publisher.Publish(new UserSignedInEvent(session), cancellationToken); return SignInCommandResult.Succeed(session); } } diff --git a/backend/src/Logitar.Master.Application/Accounts/Events/UserSignedInEvent.cs b/backend/src/Logitar.Master.Application/Accounts/Events/UserSignedInEvent.cs new file mode 100644 index 0000000..ad50e8b --- /dev/null +++ b/backend/src/Logitar.Master.Application/Accounts/Events/UserSignedInEvent.cs @@ -0,0 +1,6 @@ +using Logitar.Portal.Contracts.Sessions; +using MediatR; + +namespace Logitar.Master.Application.Accounts.Events; + +public record UserSignedInEvent(Session Session) : INotification; diff --git a/backend/src/Logitar.Master.EntityFrameworkCore/Entities/ActorEntity.cs b/backend/src/Logitar.Master.EntityFrameworkCore/Entities/ActorEntity.cs index f6341a4..ae7b82f 100644 --- a/backend/src/Logitar.Master.EntityFrameworkCore/Entities/ActorEntity.cs +++ b/backend/src/Logitar.Master.EntityFrameworkCore/Entities/ActorEntity.cs @@ -1,4 +1,6 @@ -using Logitar.Portal.Contracts.Actors; +using Logitar.EventSourcing; +using Logitar.Portal.Contracts.Actors; +using Logitar.Portal.Contracts.Users; namespace Logitar.Master.EntityFrameworkCore.Entities; @@ -14,10 +16,31 @@ internal class ActorEntity public string? EmailAddress { get; private set; } public string? PictureUrl { get; private set; } + public ActorEntity(User user) + { + Id = new ActorId(user.Id).Value; + Type = ActorType.User; + + Update(user); + } + private ActorEntity() { } + public void Update(User user) + { + string id = new ActorId(user.Id).Value; + if (Id != id || Type != ActorType.User) + { + throw new ArgumentException($"The actor '{Type}.Id={Id}' cannot be updated as the user 'User.Id={id}'.", nameof(user)); + } + + DisplayName = user.FullName ?? user.UniqueName; + EmailAddress = user.Email?.Address; + PictureUrl = user.Picture; + } + public override bool Equals(object? obj) => obj is ActorEntity actor && actor.Id == Id; public override int GetHashCode() => HashCode.Combine(GetType(), Id); public override string ToString() diff --git a/backend/src/Logitar.Master.EntityFrameworkCore/Handlers/Accounts.cs b/backend/src/Logitar.Master.EntityFrameworkCore/Handlers/Accounts.cs new file mode 100644 index 0000000..dcf601d --- /dev/null +++ b/backend/src/Logitar.Master.EntityFrameworkCore/Handlers/Accounts.cs @@ -0,0 +1,41 @@ +using Logitar.EventSourcing; +using Logitar.Master.Application.Accounts.Events; +using Logitar.Master.EntityFrameworkCore.Entities; +using Logitar.Portal.Contracts.Users; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.Master.EntityFrameworkCore.Handlers; + +internal static class Accounts +{ + public class UserSignedInEventHandler : INotificationHandler + { + private readonly MasterContext _context; + + public UserSignedInEventHandler(MasterContext context) + { + _context = context; + } + + public async Task Handle(UserSignedInEvent @event, CancellationToken cancellationToken) + { + User user = @event.Session.User; + + string id = new ActorId(user.Id).Value; + ActorEntity? actor = await _context.Actors.SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + if (actor == null) + { + actor = new(user); + + _context.Actors.Add(actor); + } + else + { + actor.Update(user); + } + + await _context.SaveChangesAsync(cancellationToken); + } + } +} diff --git a/backend/src/Logitar.Master.EntityFrameworkCore/Queriers/ProjectQuerier.cs b/backend/src/Logitar.Master.EntityFrameworkCore/Queriers/ProjectQuerier.cs index 3046748..ca6baa1 100644 --- a/backend/src/Logitar.Master.EntityFrameworkCore/Queriers/ProjectQuerier.cs +++ b/backend/src/Logitar.Master.EntityFrameworkCore/Queriers/ProjectQuerier.cs @@ -14,13 +14,13 @@ namespace Logitar.Master.EntityFrameworkCore.Queriers; internal class ProjectQuerier : IProjectQuerier { private readonly IActorService _actorService; - private readonly MasterContext _context; + private readonly DbSet _projects; private readonly ISqlHelper _sqlHelper; public ProjectQuerier(IActorService actorService, MasterContext context, ISqlHelper sqlHelper) { _actorService = actorService; - _context = context; + _projects = context.Projects; _sqlHelper = sqlHelper; } @@ -37,7 +37,7 @@ public async Task ReadAsync(ProjectAggregate project, CancellationToken { string aggregateId = new AggregateId(id).Value; - ProjectEntity? project = await _context.Projects.AsNoTracking() + ProjectEntity? project = await _projects.AsNoTracking() .SingleOrDefaultAsync(x => x.AggregateId == aggregateId, cancellationToken); return project == null ? null : await MapAsync(project, cancellationToken); @@ -47,7 +47,7 @@ public async Task ReadAsync(ProjectAggregate project, CancellationToken { string uniqueKeyNormalized = MasterDb.Normalize(uniqueKey); - ProjectEntity? project = await _context.Projects.AsNoTracking() + ProjectEntity? project = await _projects.AsNoTracking() .SingleOrDefaultAsync(x => x.UniqueKeyNormalized == uniqueKeyNormalized, cancellationToken); return project == null ? null : await MapAsync(project, cancellationToken); @@ -59,7 +59,7 @@ public async Task> SearchAsync(SearchProjectsPayload payl .ApplyIdFilter(MasterDb.Projects.AggregateId, payload.Ids); _sqlHelper.ApplyTextSearch(builder, payload.Search, MasterDb.Projects.UniqueKey, MasterDb.Projects.DisplayName); - IQueryable query = _context.Projects.FromQuery(builder).AsNoTracking(); + IQueryable query = _projects.FromQuery(builder).AsNoTracking(); long total = await query.LongCountAsync(cancellationToken); IOrderedQueryable? ordered = null; diff --git a/backend/src/Logitar.Master/Controllers/AccountController.cs b/backend/src/Logitar.Master/Controllers/AccountController.cs index 7570144..970e50b 100644 --- a/backend/src/Logitar.Master/Controllers/AccountController.cs +++ b/backend/src/Logitar.Master/Controllers/AccountController.cs @@ -26,8 +26,6 @@ public AccountController(IBearerTokenService bearerTokenService, IRequestPipelin _sessionService = sessionService; } - // TODO(fpion): get profile - [HttpPost("/auth/sign/in")] public async Task> SignInAsync([FromBody] SignInPayload payload, CancellationToken cancellationToken) { diff --git a/backend/tests/Logitar.Master.IntegrationTests/IntegrationTests.cs b/backend/tests/Logitar.Master.IntegrationTests/IntegrationTests.cs index 4e8cd54..990e93b 100644 --- a/backend/tests/Logitar.Master.IntegrationTests/IntegrationTests.cs +++ b/backend/tests/Logitar.Master.IntegrationTests/IntegrationTests.cs @@ -5,8 +5,8 @@ using Logitar.EventSourcing.EntityFrameworkCore.Relational; using Logitar.Master.Application; using Logitar.Master.Application.Accounts; -using Logitar.Master.Application.Caching; using Logitar.Master.EntityFrameworkCore; +using Logitar.Master.EntityFrameworkCore.Entities; using Logitar.Master.EntityFrameworkCore.SqlServer; using Logitar.Master.Infrastructure; using Logitar.Master.Infrastructure.Commands; @@ -101,8 +101,6 @@ protected IntegrationTests() user.CreatedBy = Actor; user.UpdatedBy = Actor; _context.User = user; - - ServiceProvider.GetRequiredService().SetActor(Actor); // TODO(fpion): insert into database } public virtual async Task InitializeAsync() @@ -115,6 +113,13 @@ public virtual async Task InitializeAsync() command.AppendLine(CreateDeleteBuilder(MasterDb.Actors.Table).Build().Text); command.AppendLine(CreateDeleteBuilder(EventDb.Events.Table).Build().Text); await MasterContext.Database.ExecuteSqlRawAsync(command.ToString()); + + if (_context.User != null) + { + ActorEntity actor = new(_context.User); + MasterContext.Actors.Add(actor); + await MasterContext.SaveChangesAsync(); + } } private IDeleteBuilder CreateDeleteBuilder(TableId table) => _databaseProvider switch {