using FastEndpoints; using KArtSell.BuildingBlocks.Data; using KArtSell.BuildingBlocks.Reliability; using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Events; using Npgsql; using System.Data; using System.Text.Json; namespace KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RegisterIdentity; public sealed class RegisterIdentityEndpoint( IRegisterIdentitySql sql, IDbConnectionFactory connectionFactory, IOutboxWriter outboxWriter) : Endpoint { public override void Configure() { Post("/api/identities"); AllowAnonymous(); } public override async Task HandleAsync(RegisterIdentityRequest req, CancellationToken ct) { var email = req.Email?.Trim().ToLowerInvariant() ?? string.Empty; if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) { await SendErrorAsync(400, "Invalid email format", ct); return; } if (string.IsNullOrWhiteSpace(req.DisplayName) || req.DisplayName.Length > 255) { await SendErrorAsync(400, "Display name required, max 255 characters", ct); return; } var emailExists = await sql.EmailExistsAsync(email, ct); if (emailExists) { await SendErrorAsync(409, "Email already registered", ct); return; } var identityId = Guid.NewGuid(); var correlationId = Guid.NewGuid().ToString(); var conn = await connectionFactory.OpenAsync(ct) as NpgsqlConnection ?? throw new InvalidOperationException("Failed to open connection"); await using (conn) { await using var transaction = await conn.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct); try { var createdId = await sql.CreateIdentityAsync(conn, transaction, identityId, email, req.DisplayName, correlationId, ct); if (createdId == Guid.Empty) { await SendErrorAsync(409, "Failed to create identity", ct); return; } // Write IdentityCreated event to Outbox var identityCreatedEvent = new IdentityCreated { IdentityId = createdId, Email = email, DisplayName = req.DisplayName, CorrelationId = correlationId, OccurredAt = DateTime.UtcNow }; var payloadJson = JsonSerializer.Serialize(identityCreatedEvent); var outboxMessage = new OutboxMessage( MessageId: Guid.NewGuid(), EventType: nameof(IdentityCreated), SchemaVersion: 1, PayloadJson: payloadJson, CorrelationId: correlationId, OccurredAt: DateTimeOffset.UtcNow, PayloadHash: ComputePayloadHash(payloadJson)); await outboxWriter.AddAsync(conn, transaction, outboxMessage, ct); await transaction.CommitAsync(ct); var (id, returnedEmail, _, currentState) = await sql.GetIdentityAsync(createdId, ct); await Send.OkAsync(new RegisterIdentityResponse { Id = id, Email = returnedEmail, State = currentState }, ct); } catch (Exception) { await transaction.RollbackAsync(ct); throw; } } } private async Task SendErrorAsync(int statusCode, string message, CancellationToken ct) { await Send.StatusCodeAsync(statusCode, ct); } private static string ComputePayloadHash(string payloadJson) { using var hasher = System.Security.Cryptography.SHA256.Create(); var hash = hasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payloadJson)); return Convert.ToHexString(hash); } }