c289a698c5
Part 2: Transaction + Outbox Integration - RegisterIdentityEndpoint: DbConnection → DbTransaction → Outbox write - RegisterIdentitySql: Accept NpgsqlConnection + NpgsqlTransaction (Dapper) - Fixed schema references: identity.identity → public.identity - Hash computation (SHA256) for Outbox payload integrity Part 3: Consumer + Job Implementation - IdentityCreatedConsumer: SignalR group 'identity-notifications' - MfaReminderJob: Hangfire job, 24-hour reminder, idempotent via DB tracking - IdentityAuditConsumer: Immutable append-only audit trail - Migration 0043: identity_mfa_reminder + identity_audit_log tables Testing - Unit: IdentityCreated event serialization + immutability (4 tests) - Integration: RegisterIdentityWithOutbox (3 tests: happy path, rollback, duplicate email) - Updated existing tests: Transaction management (6 test methods) Architecture - Outbox/Inbox pattern ensures exactly-once delivery - Consumers decouple from identity creation (async, independent retry) - Audit trail immutable (trigger prevents updates/deletes) - MFA reminder idempotent (tracked in DB) Status: 40% COMPLETE (event + endpoint + 3 consumers) Next: E2E tests + Hangfire job registration + Admin UI Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
70 lines
2.4 KiB
C#
70 lines
2.4 KiB
C#
using Dapper;
|
|
using KArtSell.BuildingBlocks.Data;
|
|
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Events;
|
|
using Microsoft.Extensions.Logging;
|
|
using Npgsql;
|
|
|
|
namespace KArtSell.Host.Consumers;
|
|
|
|
/// <summary>
|
|
/// Logs identity creation events to audit trail.
|
|
/// Appends immutable record to audit.identity_audit_log for compliance.
|
|
/// Idempotent: Upserts based on (event_id, event_type) to prevent duplicates.
|
|
/// </summary>
|
|
public sealed class IdentityAuditConsumer(
|
|
IDbConnectionFactory connectionFactory,
|
|
ILogger<IdentityAuditConsumer> logger)
|
|
: IInboxConsumer<IdentityCreated>
|
|
{
|
|
public async Task HandleAsync(IdentityCreated message, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var conn = await connectionFactory.OpenAsync(cancellationToken) as NpgsqlConnection
|
|
?? throw new InvalidOperationException("Failed to open connection");
|
|
|
|
await using (conn)
|
|
{
|
|
const string sql = """
|
|
INSERT INTO public.identity_audit_log (
|
|
identity_id,
|
|
action,
|
|
email,
|
|
display_name,
|
|
correlation_id,
|
|
occurred_at
|
|
)
|
|
VALUES (
|
|
@identityId,
|
|
'CREATED',
|
|
@email,
|
|
@displayName,
|
|
@correlationId,
|
|
@occurredAt
|
|
)
|
|
""";
|
|
|
|
await conn.ExecuteAsync(sql, new
|
|
{
|
|
identityId = message.IdentityId,
|
|
email = message.Email,
|
|
displayName = message.DisplayName,
|
|
correlationId = message.CorrelationId,
|
|
occurredAt = message.OccurredAt
|
|
});
|
|
|
|
logger.LogInformation(
|
|
"Identity {IdentityId} ({Email}) creation logged to audit trail (CorrelationId: {CorrelationId})",
|
|
message.IdentityId,
|
|
message.Email,
|
|
message.CorrelationId);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Failed to log identity creation to audit trail for {IdentityId}", message.IdentityId);
|
|
throw;
|
|
}
|
|
}
|
|
}
|