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
This commit is contained in:
Janus C. H. Knudsen 2026-01-10 11:13:33 +01:00
parent 88812177a9
commit 54b057886c
35 changed files with 1174 additions and 358 deletions

View file

@ -0,0 +1,71 @@
using System.Text.Json;
using Microsoft.ApplicationInsights;
using PlanTempus.Core.CommandQueries;
using PlanTempus.Core.Email;
using PlanTempus.Core.Outbox;
namespace PlanTempus.Components.Outbox;
public class ProcessOutboxHandler(
IOutboxService outboxService,
IEmailService emailService,
TelemetryClient telemetryClient) : ICommandHandler<ProcessOutboxCommand>
{
public async Task<CommandResponse> Handle(ProcessOutboxCommand command)
{
telemetryClient.TrackTrace($"ProcessOutboxHandler started");
var messages = await outboxService.GetPendingAsync(command.BatchSize);
telemetryClient.TrackTrace($"ProcessOutboxHandler found {messages.Count} pending messages");
foreach (var message in messages)
{
try
{
telemetryClient.TrackTrace($"Processing message {message.Id} of type {message.Type}");
await ProcessMessageAsync(message);
await outboxService.MarkAsSentAsync(message.Id);
telemetryClient.TrackTrace($"Message {message.Id} marked as sent");
}
catch (Exception ex)
{
telemetryClient.TrackTrace($"Message {message.Id} failed: {ex.Message}");
await outboxService.MarkAsFailedAsync(message.Id, ex.Message);
}
}
return new CommandResponse(command.CorrelationId, nameof(ProcessOutboxCommand), command.TransactionId);
}
private async Task ProcessMessageAsync(OutboxMessage message)
{
switch (message.Type)
{
case OutboxMessageTypes.VerificationEmail:
await ProcessVerificationEmailAsync(message);
break;
default:
throw new InvalidOperationException($"Unknown outbox message type: {message.Type}");
}
}
private async Task ProcessVerificationEmailAsync(OutboxMessage message)
{
var payload = ((JsonElement)message.Payload).Deserialize<VerificationEmailPayload>()
?? throw new InvalidOperationException("Invalid verification email payload");
var verifyUrl = $"https://plantempus.dk/confirm-email?token={payload.Token}";
var result = await emailService.SendVerificationEmailAsync(
payload.Email,
payload.UserName,
verifyUrl);
if (!result.Success)
{
throw new InvalidOperationException($"Failed to send email: {result.ErrorMessage}");
}
}
}