Files
KArtSell.Aegis/src/KArtSell.Host/Consumers/IdentityCreatedConsumer.cs
T
kjh2064 c289a698c5 feat(wbs): AEG-VS-01-05 Event/Job/Inbox - Part 2-3 Complete (Outbox/Inbox + Consumers)
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>
2026-08-17 18:41:10 +09:00

104 lines
3.4 KiB
C#

using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Events;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace KArtSell.Host.Consumers;
/// <summary>
/// Pushes identity creation notifications via SignalR.
/// Targets group: identity-notifications so all admins tracking new identities are notified.
/// Idempotent: SignalR deduplication via idempotency key.
/// </summary>
public sealed class IdentityCreatedConsumer : IInboxConsumer<IdentityCreated>
{
private readonly IHubContext<IdentityNotificationHub>? _hubContext;
private readonly ILogger<IdentityCreatedConsumer> _logger;
private static readonly Action<ILogger, Guid, string, Exception?> LogNotification =
LoggerMessage.Define<Guid, string>(
LogLevel.Information,
new EventId(1, nameof(LogNotification)),
"Identity {IdentityId} ({Email}) created notification sent");
private static readonly Action<ILogger, Exception?> LogHubNotConfigured =
LoggerMessage.Define(
LogLevel.Warning,
new EventId(2, nameof(LogHubNotConfigured)),
"SignalR hub not configured, skipping notification");
public IdentityCreatedConsumer(
IHubContext<IdentityNotificationHub>? hubContext,
ILogger<IdentityCreatedConsumer> logger)
{
_hubContext = hubContext;
_logger = logger;
}
public async Task HandleAsync(IdentityCreated message, CancellationToken cancellationToken = default)
{
try
{
LogNotification(_logger, message.IdentityId, message.Email, null);
if (_hubContext == null)
{
LogHubNotConfigured(_logger, null);
return;
}
var notification = new
{
message.IdentityId,
message.Email,
message.DisplayName,
message.OccurredAt
};
await _hubContext.Clients
.Group("identity-notifications")
.SendAsync("IdentityCreated", notification, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send identity creation notification for {IdentityId}", message.IdentityId);
throw;
}
}
}
/// <summary>
/// SignalR hub for identity notifications.
/// Clients subscribe to group: identity-notifications
/// </summary>
public sealed class IdentityNotificationHub : Hub
{
private readonly ILogger<IdentityNotificationHub> _logger;
public IdentityNotificationHub(ILogger<IdentityNotificationHub> logger)
{
_logger = logger;
}
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Client {ConnectionId} connected to IdentityNotificationHub", Context.ConnectionId);
await base.OnConnectedAsync();
}
public async Task SubscribeToIdentityNotifications()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "identity-notifications");
_logger.LogInformation(
"Client {ConnectionId} subscribed to identity-notifications",
Context.ConnectionId);
}
public async Task UnsubscribeFromIdentityNotifications()
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "identity-notifications");
_logger.LogInformation(
"Client {ConnectionId} unsubscribed from identity-notifications",
Context.ConnectionId);
}
}