diff --git a/EnvelopeGenerator.Application/Common/Dto/Signature.cs b/EnvelopeGenerator.Application/Common/Dto/Signature.cs index 9a934934..71af71eb 100644 --- a/EnvelopeGenerator.Application/Common/Dto/Signature.cs +++ b/EnvelopeGenerator.Application/Common/Dto/Signature.cs @@ -49,7 +49,7 @@ public sealed record Signature public string? Position { get => _position; - init => _position = string.IsNullOrWhiteSpace(value) ? value : null; + init => _position = string.IsNullOrWhiteSpace(value) ? null : value; } /// @@ -62,4 +62,4 @@ public sealed record Signature /// Example: "Berlin" ? rendered as "Berlin, 26.01.2025" /// public required string Place { get; init; } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Application/DependencyInjection.cs b/EnvelopeGenerator.Application/DependencyInjection.cs index 23715f65..bf41d504 100644 --- a/EnvelopeGenerator.Application/DependencyInjection.cs +++ b/EnvelopeGenerator.Application/DependencyInjection.cs @@ -65,18 +65,21 @@ public static class DependencyInjection cfg.AddBehavior, EnvelopeReceiverResolutionBehavior>(); // 1. AnnotationBehavior - Saves annotations (executes second) + cfg.AddBehavior, SaveSignatureBehavior>(); + + // 2. AnnotationBehavior - Saves annotations for legacy web flow cfg.AddBehavior, AnnotationBehavior>(); - // 2. DocStatusBehavior - Creates document status (executes third) + // 3. DocStatusBehavior - Creates document status cfg.AddBehavior, DocStatusBehavior>(); - // 3. HistoryBehavior - Records history (executes fourth) + // 4. HistoryBehavior - Records history cfg.AddBehavior, HistoryBehavior>(); - // 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed) + // 5. SendSignedMailBehavior - Sends notification email (executes LAST) cfg.AddBehavior, SendSignedMailBehavior>(); }); return services; } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs index 8eeb1669..5a1946d4 100644 --- a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs +++ b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/EnvelopeReceiverResolutionBehavior.cs @@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior x.Envelope) + .Include(x => x.Receiver) + .SingleOrDefaultAsync(cancellationToken) ?? throw new NotFoundException("EnvelopeReceiver not found"); request.SetEnvelopeReceiver(_mapper.Map(er)); diff --git a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs index e4aecec0..f1207ae3 100644 --- a/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs +++ b/EnvelopeGenerator.Application/DocReceiverElements/Behaviors/SaveSignatureBehavior.cs @@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior throw new BadRequestException($"Signatures are required for saving signature behavior."); var elements = await _elementRepo - .Where(e => e.Document.EnvelopeId == request.Envelope.Id) - .Where(e => e.ReceiverId == request.Receiver.Id) + .Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId) + .Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId) .ToListAsync(cancel); foreach (var element in elements) diff --git a/EnvelopeGenerator.Infrastructure/EGDbContext.cs b/EnvelopeGenerator.Infrastructure/EGDbContext.cs index 2b6bdee0..b66f856f 100644 --- a/EnvelopeGenerator.Infrastructure/EGDbContext.cs +++ b/EnvelopeGenerator.Infrastructure/EGDbContext.cs @@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext #region Trigger // Configure entities to handle database triggers - void AddTrigger() where T : class => _triggers - .Where(t => t.Key == typeof(T).Name) - .SelectMany(t => t.Value) - .ForEach(tName => + void AddTrigger() where T : class + { + var entityName = typeof(T).Name; + + var configuredTriggerNames = _triggers + .Where(t => t.Key == entityName) + .SelectMany(t => t.Value) + .Distinct() + .ToList(); + + foreach (var triggerName in configuredTriggerNames) { #if NET - modelBuilder.Entity().ToTable(tb => tb.HasTrigger(tName)); + modelBuilder.Entity().ToTable(tb => tb.HasTrigger(triggerName)); #endif - _logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", tName, typeof(T).Name); - }); + _logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", triggerName, entityName); + } + } // TODO: call add trigger methods with attributes and reflection AddTrigger(); @@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext #if NETFRAMEWORK } -#endif \ No newline at end of file +#endif diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureSubmitDto.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureSubmitDto.cs new file mode 100644 index 00000000..78d32317 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureSubmitDto.cs @@ -0,0 +1,10 @@ +namespace EnvelopeGenerator.Server.Client.Models; + +public sealed record SignatureSubmitDto +{ + public required int Id { get; init; } + public required string DataUrl { get; init; } + public required string FullName { get; init; } + public string? Position { get; init; } + public required string Place { get; init; } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs index a8f76d22..7b64dffb 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs @@ -1,6 +1,7 @@ using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; +using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Server.Client.Models; namespace EnvelopeGenerator.Server.Client.Services; @@ -21,4 +22,32 @@ public class SignatureService(IHttpClientFactory httpClientFactory) var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel); return result ?? []; } + + public async Task SubmitAsync(string envelopeKey, IEnumerable signatures, CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + + var payload = signatures + .Select(signature => new Signature + { + Id = signature.Id, + DataUrl = signature.DataUrl, + FullName = signature.FullName, + Position = signature.Position, + Place = signature.Place + }) + .ToList(); + + var response = await http.PostAsJsonAsync( + $"/api/Signature/submit/{Uri.EscapeDataString(envelopeKey)}", + payload, + cancel); + + if (!response.IsSuccessStatusCode) + { + var body = await response.Content.ReadAsStringAsync(cancel); + throw new HttpRequestException( + $"Failed to submit signature for envelope {envelopeKey}: {(int)response.StatusCode} {response.StatusCode} - {body}"); + } + } } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor index b1b5224f..25ebac73 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor @@ -14,6 +14,7 @@ @inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService +@inject SignatureService SignatureService @inject AppVersionService AppVersion @inject IMemoryCache MemoryCache @inject ILogger Logger @@ -125,6 +126,12 @@

+ @if (!string.IsNullOrWhiteSpace(_submitErrorMessage)) + { + + }
@@ -164,17 +171,48 @@ // ----- Submit / logout state ----- bool _isLoggingOut = false; bool _submitConfirmVisible = false; + string? _submitErrorMessage; void OpenSubmitConfirmPopup() => _submitConfirmVisible = true; async Task SubmitAndLogoutAsync() { if (_isLoggingOut) return; + + if (string.IsNullOrWhiteSpace(EnvelopeKey) || _sig is null) + return; + _isLoggingOut = true; _submitConfirmVisible = false; + _submitErrorMessage = null; await InvokeAsync(StateHasChanged); - await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!); - Navigation.NavigateTo("/envelope/signed-result", forceLoad: true); + + try + { + if (_signatures.Count == 0) + throw new InvalidOperationException("No signature fields found for receiver."); + + var payload = _signatures.Select(field => new SignatureSubmitDto + { + Id = field.Id, + DataUrl = _sig.DataUrl, + FullName = _sig.FullName, + Position = string.IsNullOrWhiteSpace(_sig.Position) ? null : _sig.Position, + Place = _sig.Place + }).ToList(); + + await SignatureService.SubmitAsync(EnvelopeKey, payload); + + await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey); + Navigation.NavigateTo("/envelope/signed-result", forceLoad: true); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to finalize signature for {EnvelopeKey}", EnvelopeKey); + _submitErrorMessage = "Die Unterschrift konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut."; + _isLoggingOut = false; + await InvokeAsync(StateHasChanged); + } } protected override async Task OnInitializedAsync() diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs index 2c951983..9e9a33cd 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs @@ -1,7 +1,10 @@ using EnvelopeGenerator.Server.Extensions; using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Extensions; +using EnvelopeGenerator.Application.DocReceiverElements.Commands; using EnvelopeGenerator.Application.Documents.Queries; +using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; +using EnvelopeGenerator.Application.Histories.Queries; using EnvelopeGenerator.Domain.Constants; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -18,13 +21,15 @@ namespace EnvelopeGenerator.Server.Controllers; public class SignatureController : ControllerBase { private readonly IMediator _mediator; + private readonly ILogger _logger; /// /// Initializes a new instance of . /// - public SignatureController(IMediator mediator) + public SignatureController(IMediator mediator, ILogger logger) { _mediator = mediator; + _logger = logger; } //TODO: update to use signature query @@ -54,4 +59,46 @@ public class SignatureController : ControllerBase else return Ok(rcvSignatures); } + + /// + /// Finalizes the receiver signing flow and persists signature data to TBSIG_DOCUMENT_RECEIVER_ELEMENT. + /// Also triggers status/history/mail side effects through SigningCommand pipeline. + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpPost("submit/{envelopeKey}")] + public async Task Submit( + [FromRoute] string envelopeKey, + [FromBody] IEnumerable? signatures, + CancellationToken cancel) + { + if (signatures is null) + return BadRequest("Signature payload is required."); + + var uuid = User.EnvelopeUuid(); + var receiverSignature = User.ReceiverSignature(); + + if (await _mediator.IsSignedAsync(uuid, receiverSignature, cancel)) + return Problem(statusCode: StatusCodes.Status409Conflict); + + if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel)) + return Problem(statusCode: StatusCodes.Status423Locked); + + try + { + await _mediator.Send(new SigningCommand + { + Envelope = new() { Uuid = uuid }, + Receiver = new() { Signature = receiverSignature }, + ReceiverAppType = ReceiverAppType.ReceiverUI, + Signatures = signatures.ToList() + }, cancel); + + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to submit signatures for envelope {EnvelopeKey}", envelopeKey); + throw; + } + } } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json index 39f160ad..64b21d1a 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json @@ -13,7 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:8088;http://localhost:5131", + "applicationUrl": "https://localhost:8089;http://localhost:5132", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json index 8e1098f6..060188ee 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json @@ -185,9 +185,10 @@ "TimeLimit": "00:30:00" }, "DbTriggerParams": { - "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], - "History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], "DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ], + "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], "EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ], "EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ], "Receiver": [],