PlanTempusApp/PlanTempus.Components/Accounts/Create/CreateAccountHandler.cs
Janus C. H. Knudsen 54b057886c Adds transactional outbox and email verification
Implements outbox pattern for reliable message delivery
Adds email verification flow with Postmark integration
Enhances account registration with secure token generation

Introduces background processing for asynchronous email sending
Implements database-level notification mechanism for message processing
2026-01-10 11:13:33 +01:00

71 lines
2.7 KiB
C#

using Insight.Database;
using Npgsql;
using PlanTempus.Components.Accounts.Exceptions;
using PlanTempus.Core;
using PlanTempus.Core.CommandQueries;
using PlanTempus.Core.Database;
using PlanTempus.Core.Outbox;
namespace PlanTempus.Components.Accounts.Create
{
public class CreateAccountHandler(
IDatabaseOperations databaseOperations,
ISecureTokenizer secureTokenizer,
IOutboxService outboxService) : ICommandHandler<CreateAccountCommand>
{
public async Task<CommandResponse> Handle(CreateAccountCommand command)
{
using var db = databaseOperations.CreateScope(nameof(CreateAccountHandler));
using var transaction = db.Connection.BeginTransaction();
try
{
var securityStamp = Guid.NewGuid().ToString("N");
var sql = @"
INSERT INTO system.accounts(email, password_hash, security_stamp, email_confirmed,
access_failed_count, lockout_enabled, is_active)
VALUES(@Email, @PasswordHash, @SecurityStamp, @EmailConfirmed,
@AccessFailedCount, @LockoutEnabled, @IsActive)
RETURNING id, created_at, email, is_active";
await db.Connection.QuerySqlAsync(sql, new
{
command.Email,
PasswordHash = secureTokenizer.TokenizeText(command.Password),
SecurityStamp = securityStamp,
EmailConfirmed = false,
AccessFailedCount = 0,
LockoutEnabled = false,
command.IsActive,
});
await outboxService.EnqueueAsync(
OutboxMessageTypes.VerificationEmail,
new VerificationEmailPayload
{
Email = command.Email,
UserName = command.Email,
Token = securityStamp
},
db.Connection,
transaction);
transaction.Commit();
return new CommandResponse(command.CorrelationId, command.GetType().Name, command.TransactionId);
}
catch (PostgresException ex) when (ex.SqlState == "23505" && ex.ConstraintName.Equals("accounts_email_key", StringComparison.InvariantCultureIgnoreCase))
{
transaction.Rollback();
db.Error(ex);
throw new EmailAlreadyRegistreredException();
}
catch (Exception ex)
{
transaction.Rollback();
db.Error(ex);
throw;
}
}
}
}