Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator
# Conflicts: # EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor # EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
This commit is contained in:
@@ -49,7 +49,7 @@ public sealed record Signature
|
|||||||
public string? Position
|
public string? Position
|
||||||
{
|
{
|
||||||
get => _position;
|
get => _position;
|
||||||
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,4 +62,4 @@ public sealed record Signature
|
|||||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Place { get; init; }
|
public required string Place { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,18 +65,21 @@ public static class DependencyInjection
|
|||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
|
|
||||||
// 1. AnnotationBehavior - Saves annotations (executes second)
|
// 1. AnnotationBehavior - Saves annotations (executes second)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
|
||||||
|
|
||||||
|
// 2. AnnotationBehavior - Saves annotations for legacy web flow
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||||
|
|
||||||
// 2. DocStatusBehavior - Creates document status (executes third)
|
// 3. DocStatusBehavior - Creates document status
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||||
|
|
||||||
// 3. HistoryBehavior - Records history (executes fourth)
|
// 4. HistoryBehavior - Records history
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||||
|
|
||||||
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
|
|||||||
// If EnvelopeReceiver is not provided, query it from database
|
// If EnvelopeReceiver is not provided, query it from database
|
||||||
if (request.EnvelopeReceiver is null)
|
if (request.EnvelopeReceiver is null)
|
||||||
{
|
{
|
||||||
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
var er = await _erRepo.Query
|
||||||
|
.Where(request, notnull: true)
|
||||||
|
.Include(x => x.Envelope)
|
||||||
|
.Include(x => x.Receiver)
|
||||||
|
.SingleOrDefaultAsync(cancellationToken)
|
||||||
?? throw new NotFoundException("EnvelopeReceiver not found");
|
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||||
|
|
||||||
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|||||||
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||||
|
|
||||||
var elements = await _elementRepo
|
var elements = await _elementRepo
|
||||||
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
||||||
.Where(e => e.ReceiverId == request.Receiver.Id)
|
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
foreach (var element in elements)
|
foreach (var element in elements)
|
||||||
|
|||||||
@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#region Trigger
|
#region Trigger
|
||||||
// Configure entities to handle database triggers
|
// Configure entities to handle database triggers
|
||||||
void AddTrigger<T>() where T : class => _triggers
|
void AddTrigger<T>() where T : class
|
||||||
.Where(t => t.Key == typeof(T).Name)
|
{
|
||||||
.SelectMany(t => t.Value)
|
var entityName = typeof(T).Name;
|
||||||
.ForEach(tName =>
|
|
||||||
|
var configuredTriggerNames = _triggers
|
||||||
|
.Where(t => t.Key == entityName)
|
||||||
|
.SelectMany(t => t.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var triggerName in configuredTriggerNames)
|
||||||
{
|
{
|
||||||
#if NET
|
#if NET
|
||||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(tName));
|
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(triggerName));
|
||||||
#endif
|
#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
|
// TODO: call add trigger methods with attributes and reflection
|
||||||
AddTrigger<Config>();
|
AddTrigger<Config>();
|
||||||
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
@@ -21,4 +22,32 @@ public class SignatureService(IHttpClientFactory httpClientFactory)
|
|||||||
var result = await response.Content.ReadFromJsonAsync<List<SignatureDto>>(_jsonOptions, cancel);
|
var result = await response.Content.ReadFromJsonAsync<List<SignatureDto>>(_jsonOptions, cancel);
|
||||||
return result ?? [];
|
return result ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SubmitAsync(string envelopeKey, IEnumerable<SignatureSubmitDto> 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
|
@inject SignatureService SignatureService
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@inject IMemoryCache MemoryCache
|
@inject IMemoryCache MemoryCache
|
||||||
@inject ILogger<ReceiverSignedPage> Logger
|
@inject ILogger<ReceiverSignedPage> Logger
|
||||||
@@ -125,6 +126,12 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_submitErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger mb-0" role="alert" style="font-size: 0.85rem;">
|
||||||
|
@_submitErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</BodyContentTemplate>
|
</BodyContentTemplate>
|
||||||
<FooterContentTemplate>
|
<FooterContentTemplate>
|
||||||
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
||||||
@@ -164,17 +171,48 @@
|
|||||||
// ----- Submit / logout state -----
|
// ----- Submit / logout state -----
|
||||||
bool _isLoggingOut = false;
|
bool _isLoggingOut = false;
|
||||||
bool _submitConfirmVisible = false;
|
bool _submitConfirmVisible = false;
|
||||||
|
string? _submitErrorMessage;
|
||||||
|
|
||||||
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
|
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
|
||||||
|
|
||||||
async Task SubmitAndLogoutAsync()
|
async Task SubmitAndLogoutAsync()
|
||||||
{
|
{
|
||||||
if (_isLoggingOut) return;
|
if (_isLoggingOut) return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _sig is null)
|
||||||
|
return;
|
||||||
|
|
||||||
_isLoggingOut = true;
|
_isLoggingOut = true;
|
||||||
_submitConfirmVisible = false;
|
_submitConfirmVisible = false;
|
||||||
|
_submitErrorMessage = null;
|
||||||
await InvokeAsync(StateHasChanged);
|
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()
|
protected override async Task OnInitializedAsync()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.Server.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
using EnvelopeGenerator.Application.Documents.Queries;
|
using EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Queries;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -18,13 +21,15 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
public class SignatureController : ControllerBase
|
public class SignatureController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
private readonly ILogger<SignatureController> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
/// Initializes a new instance of <see cref="SignatureController"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SignatureController(IMediator mediator)
|
public SignatureController(IMediator mediator, ILogger<SignatureController> logger)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: update to use signature query
|
//TODO: update to use signature query
|
||||||
@@ -54,4 +59,46 @@ public class SignatureController : ControllerBase
|
|||||||
else
|
else
|
||||||
return Ok(rcvSignatures);
|
return Ok(rcvSignatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finalizes the receiver signing flow and persists signature data to TBSIG_DOCUMENT_RECEIVER_ELEMENT.
|
||||||
|
/// Also triggers status/history/mail side effects through SigningCommand pipeline.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("submit/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> Submit(
|
||||||
|
[FromRoute] string envelopeKey,
|
||||||
|
[FromBody] IEnumerable<Signature>? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
"applicationUrl": "https://localhost:8089;http://localhost:5132",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -185,9 +185,10 @@
|
|||||||
"TimeLimit": "00:30:00"
|
"TimeLimit": "00:30:00"
|
||||||
},
|
},
|
||||||
"DbTriggerParams": {
|
"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" ],
|
"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" ],
|
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
||||||
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
||||||
"Receiver": [],
|
"Receiver": [],
|
||||||
|
|||||||
Reference in New Issue
Block a user