Add e-signature support and improve PDF signing process

Introduced `ESignParams` for configurable e-signature settings,
including `ApplicationName` and `DefaultReason`. Updated
`DocReceiverElementDto` and `PdfSignOptionsDto` to support
new properties. Registered `ESignParams` in DI container.

Refactored `SignPdfByElementCommand` to include new fields
and improved error handling. Added `SignPdfByEnvelopeReceiverCommand`
to enable signing documents for envelope receivers.

Replaced DevExpress with PdfSharp for PDF rendering in
`ReceiverSignedPage.razor`, improving layout and flexibility.
Enhanced `PdfSignatureController` with a new endpoint for
signing documents by envelope key.

Added `DataIntegrityException` for better error handling.
Updated `appsettings.json` with `ESignParams` configuration.
This commit is contained in:
2026-10-20 17:04:15 +02:00
parent e729adb441
commit b9cd629766
11 changed files with 327 additions and 96 deletions

View File

@@ -1,9 +1,12 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Enums;
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.DocReceiverElements.Queries;
using MediatR;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.PdfSigning;
@@ -21,13 +24,36 @@ public record SignPdfByElementCommand : IRequest<Stream>
///
/// </summary>
public int? ElementId { get; init; }
/// <summary>
/// Custom document
/// </summary>
public Stream? Document { get; set; }
/// <summary>
///
/// </summary>
public string? Phone { get; set; }
/// <summary>
///
/// </summary>
public string? Reason { get; set; }
/// <summary>
///
/// </summary>
public PdfSignatureCertificationLevel CertificationLevel { get; init; } = PdfSignatureCertificationLevel.NoChangesAllowed;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="signService"></param>
public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService signService, IPdfPageImageRenderer imgRenderer) : IRequestHandler<SignPdfByElementCommand, Stream>
/// <param name="imgRenderer"></param>
/// <param name="options"></param>
public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService signService, IPdfPageImageRenderer imgRenderer, IOptionsMonitor<ESignParams> options) : IRequestHandler<SignPdfByElementCommand, Stream>
{
/// <summary>
///
@@ -37,49 +63,71 @@ public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService s
/// <returns></returns>
public async Task<Stream> Handle(SignPdfByElementCommand request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// ensure element
if(request.Element is null)
if (request.Element is null)
{
var elements = await sender.Send(new ReadDocReceiverElementQuery()
{
Id = request.ElementId!,
IncludeDocument= true
IncludeDocument = request.Document is null
}, cancellationToken);
if (!elements.Any())
throw new NotFoundException($"Element with ID {request.ElementId} not found.");
request.Element = elements.FirstOrDefault();
request.Element = elements.FirstOrDefault() ?? throw new NotFoundException($"Element with ID {request.ElementId} not found."); ;
}
// ensure document
var doc = request.Element!.Document;
if(request.Document is null)
{
if (request.Element?.Document?.ByteData is not { Length: > 0 })
throw new NotFoundException("Document data is missing or empty.");
if(doc is null || doc.ByteData is null || doc.ByteData.Length == 0)
throw new NotFoundException("Document data is missing or empty.");
request.Document = new MemoryStream(request.Element.Document.ByteData);
}
ValidateAfterDataIsComplete(request);
// create signature image from element
var img = imgRenderer.ComposeSignaturePreviewPng(request.Element);
// sign pdf
var inputPdf = new MemoryStream(doc.ByteData);
var outputPdf = new MemoryStream();
await signService.SignPdfAsync(
inputPdf: inputPdf,
inputPdf: request.Document,
outputPdf: outputPdf,
options: new PdfSignOptionsDto
{
Name = request.Element.FullName!,
Location = request.Element.Position!,
Phone = request.Phone,
Reason = request.Reason ?? options.CurrentValue.DefaultReason,
Email = request.Element.Receiver!.EmailAddress,
ApplicationName = options.CurrentValue.ApplicationName,
DocumentElementId = request.Element.Id,
CertificationLevel = request.CertificationLevel,
Appearance = new PdfSignatureAppearanceDto(
PageNumber: request.Element.Page,
Left: request.Element.Left,
Bottom: request.Element.Y,
Width: request.Element.Width,
Height: request.Element.Height)
Height: request.Element.Height,
CoordinateUnit: PdfSignatureCoordinateUnit.Inch,
CoordinateOrigin: PdfSignatureCoordinateOrigin.TopLeft)
},
image: img,
cancellationToken: cancellationToken);
return outputPdf;
}
private static void ValidateAfterDataIsComplete(SignPdfByElementCommand request)
{
if(request.Element!.FullName is null)
throw new BadRequestException($"Document is not signed. FullName is required. Element ID: {request.Element.Id}");
else if(request.Element.Position is null)
throw new BadRequestException($"Document is not signed. Position is required. Element ID: {request.Element.Id}");
}
}