Compare commits
206 Commits
3f116ce11a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d39018ca39 | |||
| b49482137f | |||
| bd40404d97 | |||
| 6f16921a79 | |||
| 1afc95f9c6 | |||
| 6aed820196 | |||
| e17c4d02f8 | |||
| 8187924a8c | |||
| 1bf530f7e7 | |||
| 9cadc8e901 | |||
| 1d4ad13532 | |||
| 03a8154b1c | |||
| 20b8acd3fc | |||
| a3afeb175f | |||
| 114555c843 | |||
| f294ef2fde | |||
| 02ad819da9 | |||
| 041d98ca78 | |||
| afea2fb5ea | |||
| beeb9e4e75 | |||
| 30d13b1ffb | |||
| 814df63306 | |||
| 830d1af44a | |||
| 94018d2a36 | |||
| cf5a724bf2 | |||
| 172f2e27d7 | |||
| d350e2ae48 | |||
| 2779452d72 | |||
| 5ebc6c6739 | |||
| 891593755e | |||
| b20260674e | |||
| 7e5ff6bcb2 | |||
| 6eed9b1e31 | |||
| d4b1a4921c | |||
| f078bafdde | |||
| 786a3e128d | |||
| ff3a146636 | |||
| 40b2cad598 | |||
| 5c675be0ed | |||
| 58164be640 | |||
| a639377195 | |||
| e3d6e87ee5 | |||
| 2795b91386 | |||
| ca248c3aa6 | |||
| 383634fca6 | |||
| 75097afa06 | |||
| 77975c0644 | |||
| 5707213edd | |||
| ad54ba9dc4 | |||
| 1f233153cf | |||
| 513ec007eb | |||
| 1305714da2 | |||
| 1e90cda393 | |||
| 5a5cbcb14d | |||
| a35f06070a | |||
| 2606066103 | |||
| 7495e062a9 | |||
| 293044bec3 | |||
| e0ff976d21 | |||
| bec45ab1f1 | |||
| fecd054a5c | |||
| 32b488c50f | |||
| 9cfdd16970 | |||
| 4da5848253 | |||
| 88da210ba2 | |||
| fc23ba840e | |||
| 140d271b28 | |||
| a3b12a6957 | |||
| 16bdc7820d | |||
| 06e32b99ea | |||
| c7c78f96a6 | |||
| 5c232e61f2 | |||
| 24c9321c0f | |||
| c75c2b1dd5 | |||
| 8445757f34 | |||
| b088eb089f | |||
| e66c46767e | |||
| bc732d311c | |||
| c90d29d654 | |||
| 47a2e950ca | |||
| 6ef989213e | |||
| 2a27b6161b | |||
| efdc372b04 | |||
| 698b7ca1ac | |||
| bf6947a28c | |||
| e2e31e2e69 | |||
| 73f6221c3c | |||
| 10f730a833 | |||
| cf5a301942 | |||
| e364f1f592 | |||
| 8a488d4e71 | |||
| f0be1a5b03 | |||
| 773721b634 | |||
| 99e3e4c24d | |||
| b9c86ce3c6 | |||
| 637b45efe0 | |||
| 28b8c311f9 | |||
| 00c7fe5316 | |||
| e5a061d5b5 | |||
| 629b02863b | |||
| 3b24755c35 | |||
| 864e9e8164 | |||
| 7eff958d0a | |||
| c3deaae63b | |||
| bb0197e6ba | |||
| ec2935b524 | |||
| 4fd7982cba | |||
| ddcf5edc00 | |||
| 74d207caa3 | |||
| a367c12551 | |||
| 35a328f8dc | |||
| d259a15b4b | |||
| 23e0e5ddbe | |||
| 0bb85c28c1 | |||
| a11d9a0e0e | |||
| b9bb058137 | |||
| 0818d7d9eb | |||
| 9d200800c5 | |||
| 6feb601670 | |||
| 39c12ada45 | |||
| 985ad4dc29 | |||
| 038ac2aed0 | |||
| 5e74de0ce7 | |||
| 0ce7ae9494 | |||
| 7041a4694a | |||
| 75e47d10e3 | |||
| 7f9125b3aa | |||
| fee256a51a | |||
| 8ad7c37261 | |||
| ef28bbaaf1 | |||
| 258de6244c | |||
| a845b85a5c | |||
| 02a7b706cf | |||
| 7912469709 | |||
| 75d975223e | |||
| c456d67d03 | |||
| 241e59fc7e | |||
| f0d101bb23 | |||
| 8db5afae40 | |||
| b62cca5961 | |||
| 0e7b120ded | |||
| d8cbdb0c65 | |||
| 0107602a84 | |||
| 02ecd88758 | |||
| 17c7e46388 | |||
| f3af30c67d | |||
| 90e10d3d04 | |||
| af14ef7ce5 | |||
| 1edcfed318 | |||
| 2004c7ced2 | |||
| 40135fb8a2 | |||
| b57c0aa9c7 | |||
| 2c4c18935f | |||
| d8ed06fdb6 | |||
| 09bf8db884 | |||
| 911c812b19 | |||
| 8ae0f79365 | |||
| 0ca54fe1fe | |||
| a1d6b5347f | |||
| 6cc631111c | |||
| 9d6074874f | |||
| 26bdb0806d | |||
| 7919f02ffd | |||
| 04ae14c660 | |||
| cff79730b0 | |||
| 188cb67306 | |||
| abaa315b24 | |||
| 4f463c27e6 | |||
| d6f17ec4e8 | |||
| e3e2831da1 | |||
| 52306d481f | |||
| f046be240b | |||
| 16e5d5c692 | |||
| e64ad44b71 | |||
| e88bd55198 | |||
| 4abed0e1bc | |||
| 69821e64c6 | |||
| f13a2434f7 | |||
| ecc7552951 | |||
| d10f19d92a | |||
| 5e53f2b691 | |||
| f56928f44f | |||
| faa37e0dcd | |||
| e51470a449 | |||
| adce61fead | |||
| e052bf56f4 | |||
| 22a7619627 | |||
| 281cf47834 | |||
| a258dcdad0 | |||
| 79c26eb5b5 | |||
| 1f745ae79c | |||
| ce7ca39c39 | |||
| 7b6f916486 | |||
| 57422a481c | |||
| e96523b786 | |||
| 3b7d0e1321 | |||
| 9adc1ea4e7 | |||
| 510f5e9ddd | |||
| 44b204ca68 | |||
| b72ac68daf | |||
| 7d85d59ace | |||
| 4fad41bd0b | |||
| 39936792aa | |||
| 74f444a8d6 | |||
| b67f26cc21 | |||
| a29f918125 |
73
EnvelopeGenerator.Application/Common/Dto/AnnotationDto.cs
Normal file
73
EnvelopeGenerator.Application/Common/Dto/AnnotationDto.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record AnnotationCreateDto
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int ElementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Name { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Value { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Type { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? X { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? Y { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? Height { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record AnnotationDto : AnnotationCreateDto
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DateTime? ChangedWhen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? ChangedWho { get; init; }
|
||||
}
|
||||
@@ -74,6 +74,11 @@ public record EnvelopeDto
|
||||
/// </summary>
|
||||
public int? EnvelopeTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool ReadOnly => EnvelopeTypeId == 2;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -82,7 +87,7 @@ public record EnvelopeDto
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool? UseAccessCode { get; set; }
|
||||
public bool UseAccessCode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
||||
@@ -35,6 +35,7 @@ public class MappingProfile : Profile
|
||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||
|
||||
// DTO to Entity mappings
|
||||
CreateMap<ConfigDto, Config>();
|
||||
@@ -50,6 +51,8 @@ public class MappingProfile : Profile
|
||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||
.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
|
||||
|
||||
// Messaging mappings
|
||||
// for GTX messaging
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace EnvelopeGenerator.Application.Common.Extensions;
|
||||
/// <summary>
|
||||
/// Extension methods for tasks
|
||||
/// </summary>
|
||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
||||
public static class TaskExtensions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -17,6 +18,7 @@ public static class TaskExtensions
|
||||
/// <param name="factory">Exception provider</param>
|
||||
/// <returns>The awaited result if not <c>null</c>.</returns>
|
||||
/// <exception>Thrown if the result is <c>null</c>.</exception>
|
||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
||||
public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception
|
||||
{
|
||||
var result = await task;
|
||||
@@ -33,6 +35,7 @@ public static class TaskExtensions
|
||||
/// <param name="factory">Exception provider</param>
|
||||
/// <returns>The awaited collection if it is not <c>null</c> or empty.</returns>
|
||||
/// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception>
|
||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
||||
public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception
|
||||
{
|
||||
var result = await task;
|
||||
@@ -47,11 +50,33 @@ public static class TaskExtensions
|
||||
/// <param name="task"></param>
|
||||
/// <param name="act"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
||||
public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act)
|
||||
{
|
||||
var res = await task;
|
||||
return act(res);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="task"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
||||
public static Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task) => task.Then(t => t.FirstOrDefault());
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <typeparam name="TException"></typeparam>
|
||||
/// <param name="task"></param>
|
||||
/// <param name="factory"></param>
|
||||
/// <returns></returns>
|
||||
public static Task<T> FirstAsync<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory)
|
||||
where TException : Exception
|
||||
=> task.Then(t => t.FirstOrDefault() ?? throw factory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,11 +93,13 @@ public static class Exceptions
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
||||
public static BadRequestException BadRequest() => new();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
||||
public static ForbiddenException Forbidden() => new();
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Newtonsoft.Json;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Instant"></param>
|
||||
/// <param name="Structured"></param>
|
||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -16,7 +24,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required ExpandoObject Annotations { get; init; }
|
||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -40,17 +48,41 @@ public static class DocSignedNotificationExtensions
|
||||
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
||||
/// </summary>
|
||||
/// <param name="dto">The DTO to convert.</param>
|
||||
/// <param name="annotations"></param>
|
||||
/// <param name="psPdfKitAnnotation"></param>
|
||||
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, ExpandoObject annotations)
|
||||
=> new(dto) { Annotations = annotations };
|
||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
|
||||
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously converts a <see cref="Task{EnvelopeReceiverDto}"/> to a <see cref="DocSignedNotification"/>.
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="dtoTask">The task that returns the DTO to convert.</param>
|
||||
/// <param name="annotations"></param>
|
||||
/// <returns>A task that represents the asynchronous conversion operation.</returns>
|
||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, ExpandoObject annotations)
|
||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { Annotations = annotations } : null;
|
||||
/// <param name="dtoTask"></param>
|
||||
/// <param name="psPdfKitAnnotation"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
|
||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="publisher"></param>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await publisher.Publish(notification, cancel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await publisher.Publish(new RemoveSignatureNotification()
|
||||
{
|
||||
EnvelopeId = notification.EnvelopeId,
|
||||
ReceiverId = notification.ReceiverId
|
||||
}, cancel);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
|
||||
@@ -10,15 +9,18 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// </summary>
|
||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private readonly IRepository<ElementAnnotation> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
public AnnotationHandler(ISender sender)
|
||||
/// <param name="repository"></param>
|
||||
public AnnotationHandler(IRepository<ElementAnnotation> repository)
|
||||
{
|
||||
_sender = sender;
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -29,11 +31,7 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <returns></returns>
|
||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
{
|
||||
await _sender.Send(new SaveDocStatusCommand()
|
||||
{
|
||||
Envelope = new() { Id = notification.EnvelopeId },
|
||||
Receiver = new() { Id = notification.ReceiverId},
|
||||
Value = JsonSerializer.Serialize(notification.Annotations, Format.Json.ForAnnotations)
|
||||
}, cancel);
|
||||
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
|
||||
await _repo.CreateAsync(annot.Structured, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private const string BlankAnnotationJson = "{}";
|
||||
|
||||
private readonly ISender _sender;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
public DocStatusHandler(ISender sender)
|
||||
{
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
{
|
||||
await _sender.Send(new SaveDocStatusCommand()
|
||||
{
|
||||
Envelope = new() { Id = notification.EnvelopeId },
|
||||
Receiver = new() { Id = notification.ReceiverId},
|
||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||
: BlankAnnotationJson
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
using EnvelopeGenerator.Application.Histories.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class RemoveAnnotationHandler : INotificationHandler<RemoveSignatureNotification>
|
||||
{
|
||||
private readonly IRepository<ElementAnnotation> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
public RemoveAnnotationHandler(IRepository<ElementAnnotation> repository)
|
||||
{
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
||||
{
|
||||
notification.ThrowIfHasNoFilter();
|
||||
return _repo.DeleteAsync(annots =>
|
||||
{
|
||||
// envelope ID filter
|
||||
if (notification.EnvelopeId is int envelopeId)
|
||||
annots = annots.Where(annot => annot.Element!.Document.EnvelopeId == envelopeId);
|
||||
|
||||
// envelope UUID filter
|
||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
||||
annots = annots.Where(annot => annot.Element!.Document.Envelope!.Uuid == envelopeUuid);
|
||||
|
||||
// receiver ID
|
||||
if (notification.ReceiverId is int receiverId)
|
||||
annots = annots.Where(annot => annot.Element!.ReceiverId == receiverId);
|
||||
|
||||
// receiver signature
|
||||
if (notification.ReceiverSignature is string receiverSignature)
|
||||
annots = annots.Where(annot => annot.Element!.Receiver!.Signature == receiverSignature);
|
||||
|
||||
return annots;
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using AngleSharp.Html;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class RemoveDocStatusHandler : INotificationHandler<RemoveSignatureNotification>
|
||||
{
|
||||
private readonly IRepository<Domain.Entities.DocumentStatus> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
public RemoveDocStatusHandler(IRepository<Domain.Entities.DocumentStatus> repository)
|
||||
{
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
||||
{
|
||||
notification.ThrowIfHasNoFilter();
|
||||
return _repo.DeleteAsync(statuses =>
|
||||
{
|
||||
// envelope ID filter
|
||||
if (notification.EnvelopeId is int envelopeId)
|
||||
statuses = statuses.Where(status => status.EnvelopeId == envelopeId);
|
||||
|
||||
// envelope UUID filter
|
||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
||||
statuses = statuses.Where(status => status.Envelope!.Uuid == envelopeUuid);
|
||||
|
||||
// receiver Id filter
|
||||
if (notification.ReceiverId is int receiverId)
|
||||
statuses = statuses.Where(status => status.ReceiverId == receiverId);
|
||||
|
||||
// receiver signature filter
|
||||
if (notification.ReceiverSignature is string receiverSignature)
|
||||
statuses = statuses.Where(status => status.Receiver!.Signature == receiverSignature);
|
||||
|
||||
return statuses;
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class RemoveHistoryHandler : INotificationHandler<RemoveSignatureNotification>
|
||||
{
|
||||
private readonly IRepository<Domain.Entities.History> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
public RemoveHistoryHandler(IRepository<Domain.Entities.History> repository)
|
||||
{
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
||||
{
|
||||
notification.ThrowIfHasNoFilter();
|
||||
return _repo.DeleteAsync(hists =>
|
||||
{
|
||||
hists = hists.Where(hist => hist.Status == EnvelopeStatus.DocumentSigned);
|
||||
|
||||
// envelope ID filter
|
||||
if (notification.EnvelopeId is int envelopeId)
|
||||
hists = hists.Where(hist => hist.EnvelopeId == envelopeId);
|
||||
|
||||
// envelope UUID filter
|
||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
||||
hists = hists.Where(hist => hist.Envelope!.Uuid == envelopeUuid);
|
||||
|
||||
// receiver ID filter
|
||||
if (notification.ReceiverId is int receiverId)
|
||||
hists = hists.Where(hist => hist.Receiver!.Id == receiverId);
|
||||
|
||||
// receiver signature filter
|
||||
if (notification.ReceiverSignature is string receiverSignature)
|
||||
hists = hists.Where(hist => hist.Receiver!.Signature == receiverSignature);
|
||||
|
||||
return hists;
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="EnvelopeId"></param>
|
||||
/// <param name="ReceiverId"></param>
|
||||
/// <param name="EnvelopeUuid"></param>
|
||||
/// <param name="ReceiverSignature"></param>
|
||||
public record RemoveSignatureNotification(
|
||||
int? EnvelopeId = null,
|
||||
int? ReceiverId = null,
|
||||
string? EnvelopeUuid = null,
|
||||
string? ReceiverSignature = null
|
||||
) : INotification
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool HasFilter =>
|
||||
EnvelopeId is not null
|
||||
|| ReceiverId is not null
|
||||
|| EnvelopeUuid is not null
|
||||
|| ReceiverSignature is not null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public void ThrowIfHasNoFilter()
|
||||
{
|
||||
if (!HasFilter)
|
||||
throw new InvalidOperationException("At least one filter parameter must be provided.");
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.3.5" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
|
||||
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using AutoMapper;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
@@ -47,7 +47,13 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
/// Die Antwort enthält Details wie den Include, die Zuordnung zwischen Umschlag und Empfänger
|
||||
/// sowie zusätzliche Metadaten.
|
||||
/// </remarks>
|
||||
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>;
|
||||
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Optionaler Benutzernamefilter, um Ergebnisse auf Umschläge eines bestimmten Besitzers einzuschränken.
|
||||
/// </summary>
|
||||
public string? Username { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -82,73 +88,74 @@ public static class Extensions
|
||||
q.Receiver.Signature = signature;
|
||||
return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
|
||||
{
|
||||
private readonly IRepository<EnvelopeReceiver> _repo;
|
||||
|
||||
private readonly IRepository<Receiver> _rcvRepo;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Verarbeitet <see cref="ReadEnvelopeReceiverQuery"/> und liefert passende <see cref="EnvelopeReceiverDto"/>-Ergebnisse.
|
||||
/// </summary>
|
||||
/// <param name="envelopeReceiver"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
|
||||
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
|
||||
{
|
||||
_repo = envelopeReceiver;
|
||||
_mapper = mapper;
|
||||
_rcvRepo = rcvRepo;
|
||||
}
|
||||
private readonly IRepository<EnvelopeReceiver> _repo;
|
||||
private readonly IRepository<Receiver> _rcvRepo;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="BadRequestException"></exception>
|
||||
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
|
||||
{
|
||||
var q = _repo.ReadOnly().Where(request, notnull: false);
|
||||
|
||||
if (request.Envelope.Status is not null)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeReceiver"></param>
|
||||
/// <param name="rcvRepo"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
|
||||
{
|
||||
var status = request.Envelope.Status;
|
||||
if (status.Min is not null)
|
||||
q = q.Where(er => er.Envelope!.Status >= status.Min);
|
||||
|
||||
if (status.Max is not null)
|
||||
q = q.Where(er => er.Envelope!.Status <= status.Max);
|
||||
|
||||
if (status.Include?.Length > 0)
|
||||
q = q.Where(er => status.Include.Contains(er.Envelope!.Status));
|
||||
|
||||
if (status.Ignore is not null)
|
||||
q = q.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
||||
_repo = envelopeReceiver;
|
||||
_mapper = mapper;
|
||||
_rcvRepo = rcvRepo;
|
||||
}
|
||||
|
||||
var envRcvs = await q
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.User)
|
||||
.Include(er => er.Receiver)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
if (request.Receiver.HasAnyCriteria && envRcvs.Any())
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="BadRequestException"></exception>
|
||||
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
|
||||
{
|
||||
var receiver = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
|
||||
var q = _repo.Query.Where(request, notnull: false);
|
||||
|
||||
foreach (var envRcv in envRcvs)
|
||||
envRcv.Envelope?.Documents?.First().Elements.RemoveAll(s => s.ReceiverId != receiver.Id);
|
||||
if (request.Username is string username)
|
||||
q = q.Where(er => er.Envelope!.User.Username == username);
|
||||
|
||||
if (request.Envelope.Status is not null)
|
||||
{
|
||||
var status = request.Envelope.Status;
|
||||
if (status.Min is not null)
|
||||
q = q.Where(er => er.Envelope!.Status >= status.Min);
|
||||
|
||||
if (status.Max is not null)
|
||||
q = q.Where(er => er.Envelope!.Status <= status.Max);
|
||||
|
||||
if (status.Include?.Length > 0)
|
||||
q = q.Where(er => status.Include.Contains(er.Envelope!.Status));
|
||||
|
||||
if (status.Ignore is not null)
|
||||
q = q.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
||||
}
|
||||
|
||||
var envRcvs = await q
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.User)
|
||||
.Include(er => er.Receiver)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0)
|
||||
{
|
||||
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel);
|
||||
|
||||
foreach (var envRcv in envRcvs)
|
||||
envRcv.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id);
|
||||
}
|
||||
|
||||
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
|
||||
}
|
||||
|
||||
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using MediatR;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.EnvelopeTypes.Queries;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
||||
{
|
||||
private readonly IRepository<EnvelopeType> _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var types = await _repository.Query.ToListAsync(cancellationToken);
|
||||
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,18 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||
/// <summary>
|
||||
/// ID des Absenders
|
||||
/// </summary>
|
||||
public int UserId { get; set; }
|
||||
internal int UserId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public bool Authorize(int userId)
|
||||
{
|
||||
UserId = userId;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines which component is used for envelope processing.
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Repräsentiert eine Abfrage für Umschläge.
|
||||
/// </summary>
|
||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest
|
||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Abfrage des Include des Umschlags
|
||||
/// </summary>
|
||||
public EnvelopeStatusQuery? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
||||
/// </summary>
|
||||
public int? UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Setzt den Benutzerkontext für die Abfrage.
|
||||
/// </summary>
|
||||
public ReadEnvelopeQuery Authorize(int userId) => this with { UserId = userId };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,4 +80,62 @@ public record EnvelopeStatusQuery
|
||||
/// Eine Liste von Statuswerten, die ignoriert werden werden.
|
||||
/// </summary>
|
||||
public EnvelopeStatus[]? Ignore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verarbeitet <see cref="ReadEnvelopeQuery"/> und liefert passende <see cref="EnvelopeDto"/>-Ergebnisse.
|
||||
/// </summary>
|
||||
public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnumerable<EnvelopeDto>>
|
||||
{
|
||||
private readonly IRepository<Envelope> _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadEnvelopeQueryHandler(IRepository<Envelope> repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<EnvelopeDto>> Handle(ReadEnvelopeQuery request, CancellationToken cancel)
|
||||
{
|
||||
var query = _repository.Query;
|
||||
|
||||
if (request.UserId is int userId)
|
||||
query = query.Where(e => e.UserId == userId);
|
||||
|
||||
if (request.Id is int id)
|
||||
query = query.Where(e => e.Id == id);
|
||||
|
||||
if (request.Uuid is string uuid)
|
||||
query = query.Where(e => e.Uuid == uuid);
|
||||
|
||||
if (request.Status is { } status)
|
||||
{
|
||||
if (status.Min is not null)
|
||||
query = query.Where(e => e.Status >= status.Min);
|
||||
if (status.Max is not null)
|
||||
query = query.Where(e => e.Status <= status.Max);
|
||||
if (status.Include?.Length > 0)
|
||||
query = query.Where(e => status.Include.Contains(e.Status));
|
||||
if (status.Ignore?.Length > 0)
|
||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||
}
|
||||
|
||||
var envelopes = await query
|
||||
.Include(e => e.Documents)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
|
||||
@@ -6,6 +10,54 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
/// Eine Abfrage, um die zuletzt verwendete Anrede eines Empfängers zu ermitteln,
|
||||
/// damit diese für zukünftige Umschläge wiederverwendet werden kann.
|
||||
/// </summary>
|
||||
public record ReadReceiverNameQuery() : ReadReceiverQuery
|
||||
public record ReadReceiverNameQuery() : ReceiverQueryBase, IRequest<string?>;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadReceiverNameQueryHandler : IRequestHandler<ReadReceiverNameQuery, string?>
|
||||
{
|
||||
}
|
||||
private readonly IRepository<EnvelopeReceiver> _envelopeReceiverRepository;
|
||||
private readonly IRepository<Receiver> _receiverRepository;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeReceiverRepository"></param>
|
||||
/// <param name="receiverRepository"></param>
|
||||
public ReadReceiverNameQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiverRepository, IRepository<Receiver> receiverRepository)
|
||||
{
|
||||
_envelopeReceiverRepository = envelopeReceiverRepository;
|
||||
_receiverRepository = receiverRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<string?> Handle(ReadReceiverNameQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var receiverQuery = _receiverRepository.Query.AsNoTracking();
|
||||
|
||||
if (request.Id is int id)
|
||||
receiverQuery = receiverQuery.Where(r => r.Id == id);
|
||||
if (request.EmailAddress is string email)
|
||||
receiverQuery = receiverQuery.Where(r => r.EmailAddress == email);
|
||||
if (request.Signature is string signature)
|
||||
receiverQuery = receiverQuery.Where(r => r.Signature == signature);
|
||||
|
||||
var receiver = await receiverQuery.FirstOrDefaultAsync(cancellationToken);
|
||||
if (receiver is null)
|
||||
return null;
|
||||
|
||||
var erName = await _envelopeReceiverRepository.Query
|
||||
.Where(er => er.ReceiverId == receiver.Id)
|
||||
.OrderByDescending(er => er.AddedWhen)
|
||||
.Select(er => er.Name)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return erName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
//TODO: Add sender query
|
||||
/// <summary>
|
||||
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
|
||||
/// </summary>
|
||||
public record CountHistoryQuery : HistoryQueryBase, IRequest<int>;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class CountHistoryQueryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="uuid"></param>
|
||||
/// <param name="statuses"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> AnyHistoryAsync(this ISender sender, string uuid, IEnumerable<EnvelopeStatus> statuses, CancellationToken cancel = default)
|
||||
{
|
||||
var count = await sender.Send(new CountHistoryQuery
|
||||
{
|
||||
Envelope = new() { Uuid = uuid },
|
||||
Statuses = new() { Include = statuses }
|
||||
}, cancel);
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class CountHistoryQueryHandler : IRequestHandler<CountHistoryQuery, int>
|
||||
{
|
||||
private readonly IRepository<History> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public CountHistoryQueryHandler(IRepository<History> repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public Task<int> Handle(CountHistoryQuery request, CancellationToken cancel = default)
|
||||
{
|
||||
var query = _repo.Query;
|
||||
|
||||
if (request.Envelope.Id is int envId)
|
||||
query = query.Where(e => e.Id == envId);
|
||||
else if (request.Envelope.Uuid is string uuid)
|
||||
query = query.Where(e => e.Envelope!.Uuid == uuid);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
else if (request.EnvelopeId is not null)
|
||||
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
else
|
||||
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (request.Status is not null)
|
||||
query = query.Where(h => h.Status == request.Status);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
if (request.Statuses is not null)
|
||||
{
|
||||
var status = request.Statuses;
|
||||
if (status.Min is not null)
|
||||
query = query.Where(er => er.Envelope!.Status >= status.Min);
|
||||
|
||||
if (status.Max is not null)
|
||||
query = query.Where(er => er.Envelope!.Status <= status.Max);
|
||||
|
||||
if (status.Include?.Count() > 0)
|
||||
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
|
||||
|
||||
if (status.Ignore is not null)
|
||||
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
||||
}
|
||||
|
||||
return query.CountAsync(cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
//TODO: Add sender query
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record HistoryQueryBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Die eindeutige Kennung des Umschlags.
|
||||
/// </summary>
|
||||
[Obsolete("Use Envelope property")]
|
||||
public int? EnvelopeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
|
||||
/// </summary>
|
||||
[Obsolete("Use statuses")]
|
||||
public EnvelopeStatus? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public EnvelopeStatusQuery Statuses { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public EnvelopeQueryBase Envelope { get; set; } = new EnvelopeQueryBase();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record EnvelopeStatusQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Der minimale Statuswert, der berücksichtigt werden.
|
||||
/// </summary>
|
||||
public EnvelopeStatus? Min { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Der maximale Statuswert, der berücksichtigt werden.
|
||||
/// </summary>
|
||||
public EnvelopeStatus? Max { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Eine Liste von Statuswerten, die einbezogen werden.
|
||||
/// </summary>
|
||||
public IEnumerable<EnvelopeStatus>? Include { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Eine Liste von Statuswerten, die ignoriert werden werden.
|
||||
/// </summary>
|
||||
public IEnumerable<EnvelopeStatus>? Ignore { get; init; }
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
@@ -9,21 +12,81 @@ namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
/// <summary>
|
||||
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
|
||||
/// </summary>
|
||||
public record ReadHistoryQuery : IRequest<IEnumerable<HistoryDto>>
|
||||
public record ReadHistoryQuery : HistoryQueryBase, IRequest<IEnumerable<HistoryDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Die eindeutige Kennung des Umschlags.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
|
||||
/// </summary>
|
||||
public EnvelopeStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Abfrage zur Steuerung, ob nur der aktuelle Include oder der gesamte Datensatz zurückgegeben wird.
|
||||
/// </summary>
|
||||
public bool? OnlyLast { get; init; } = true;
|
||||
public bool OnlyLast { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
|
||||
{
|
||||
private readonly IRepository<History> _repo;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
|
||||
{
|
||||
_repo = repo;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
|
||||
{
|
||||
var query = _repo.Query;
|
||||
|
||||
if (request.Envelope.Id is int envId)
|
||||
query = query.Where(e => e.Id == envId);
|
||||
else if (request.Envelope.Uuid is string uuid)
|
||||
query = query.Where(e => e.Envelope!.Uuid == uuid);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
else if (request.EnvelopeId is not null)
|
||||
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
else
|
||||
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (request.Status is not null)
|
||||
query = query.Where(h => h.Status == request.Status);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
if (request.Statuses is not null)
|
||||
{
|
||||
var status = request.Statuses;
|
||||
if (status.Min is not null)
|
||||
query = query.Where(er => er.Envelope!.Status >= status.Min);
|
||||
|
||||
if (status.Max is not null)
|
||||
query = query.Where(er => er.Envelope!.Status <= status.Max);
|
||||
|
||||
if (status.Include?.Count() > 0)
|
||||
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
|
||||
|
||||
if (status.Ignore is not null)
|
||||
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
||||
}
|
||||
|
||||
if (request.OnlyLast)
|
||||
query = query.OrderByDescending(x => x.AddedWhen);
|
||||
|
||||
var hists = await query.ToListAsync(cancel);
|
||||
return _mapper.Map<List<HistoryDto>>(hists);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
|
||||
{
|
||||
private readonly IRepository<History> _repo;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
|
||||
{
|
||||
_repo = repo;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
|
||||
{
|
||||
var query = _repo.ReadOnly().Where(h => h.EnvelopeId == request.EnvelopeId);
|
||||
if (request.Status is not null)
|
||||
query = query.Where(h => h.Status == request.Status);
|
||||
|
||||
var hists = await query.ToListAsync(cancel);
|
||||
return _mapper.Map<List<HistoryDto>>(hists);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
||||
|
||||
@@ -6,4 +12,53 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
|
||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||
/// um spezifische Informationen über einen Empfänger abzurufen.
|
||||
/// </summary>
|
||||
public record ReadReceiverQuery : ReceiverQueryBase;
|
||||
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnumerable<ReceiverDto>>
|
||||
{
|
||||
private readonly IRepository<Receiver> _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadReceiverQueryHandler(IRepository<Receiver> repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<ReceiverDto>> Handle(ReadReceiverQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _repository.Query;
|
||||
|
||||
if (request.Id is int id)
|
||||
{
|
||||
query = query.Where(r => r.Id == id);
|
||||
}
|
||||
|
||||
if (request.EmailAddress is string email)
|
||||
{
|
||||
query = query.Where(r => r.EmailAddress == email);
|
||||
}
|
||||
|
||||
if (request.Signature is string signature)
|
||||
{
|
||||
query = query.Where(r => r.Signature == signature);
|
||||
}
|
||||
|
||||
var receiver = await query.ToListAsync(cancellationToken);
|
||||
return _mapper.Map<IEnumerable<ReceiverDto>>(receiver);
|
||||
}
|
||||
}
|
||||
@@ -70,11 +70,11 @@
|
||||
<Reference Include="DigitalData.Controls.DocumentViewer, Version=1.9.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Controls.DocumentViewer.1.9.8\lib\net462\DigitalData.Controls.DocumentViewer.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.3.5.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.3.5\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.1.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.1.1\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Modules.Base, Version=1.3.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Modules.Base.1.3.8\lib\net462\DigitalData.Modules.Base.dll</HintPath>
|
||||
@@ -109,9 +109,6 @@
|
||||
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.SqlServer.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="EnvelopeGenerator.CommonServices">
|
||||
<HintPath>..\EnvelopeGenerator.CommonServices\bin\Debug\EnvelopeGenerator.CommonServices.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="FirebirdSql.Data.FirebirdClient, Version=7.5.0.0, Culture=neutral, PublicKeyToken=3750abcc3150b00c, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\FirebirdSql.Data.FirebirdClient.7.5.0\lib\net452\FirebirdSql.Data.FirebirdClient.dll</HintPath>
|
||||
</Reference>
|
||||
@@ -466,8 +463,12 @@
|
||||
<Project>{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}</Project>
|
||||
<Name>EnvelopeGenerator.CommonServices</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj">
|
||||
<Project>{6ea0c51f-c2b1-4462-8198-3de0b32b74f8}</Project>
|
||||
<Name>EnvelopeGenerator.CommonServices</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj">
|
||||
<Project>{4F32A98D-E6F0-4A09-BD97-1CF26107E837}</Project>
|
||||
<Project>{4f32a98d-e6f0-4a09-bd97-1cf26107e837}</Project>
|
||||
<Name>EnvelopeGenerator.Domain</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj">
|
||||
@@ -495,6 +496,9 @@
|
||||
<Content Include="MailLicense.xml" />
|
||||
<Content Include="README.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<WCFMetadata Include="Connected Services\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
|
||||
<Import Project="..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets" Condition="Exists('..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
|
||||
@@ -5,9 +5,10 @@ Imports EnvelopeGenerator.CommonServices.Jobs
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||
Imports GdPicture14
|
||||
Imports Newtonsoft.Json.Linq
|
||||
Imports DigitalData.Core.Abstraction.Application
|
||||
Imports EnvelopeGenerator.Infrastructure
|
||||
Imports Microsoft.EntityFrameworkCore
|
||||
Imports System.Text
|
||||
Imports DigitalData.Core.Abstractions
|
||||
|
||||
Public Class frmFinalizePDF
|
||||
Private Const CONNECTIONSTRING = "Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=+bk8oAbbQP1AzoHtvZUbd+Mbok2f8Fl4miEx1qssJ5yEaEWoQJ9prg4L14fURpPnqi1WMNs9fE4=;"
|
||||
@@ -23,10 +24,15 @@ Public Class frmFinalizePDF
|
||||
|
||||
Private Sub frmFinalizePDF_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
||||
LogConfig = New LogConfig(LogConfig.PathType.CustomPath, Application.StartupPath)
|
||||
Database = New MSSQLServer(LogConfig, MSSQLServer.DecryptConnectionString(CONNECTIONSTRING))
|
||||
|
||||
Dim dCnnStr As String = MSSQLServer.DecryptConnectionString(CONNECTIONSTRING)
|
||||
|
||||
Database = New MSSQLServer(LogConfig, dCnnStr)
|
||||
|
||||
#Disable Warning BC40000 ' Type or member is obsolete
|
||||
Factory.Shared.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Factory.Shared _
|
||||
.BehaveOnPostBuild(PostBuildBehavior.Ignore) _
|
||||
.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Sub(opt)
|
||||
opt.AddDbTriggerParams(
|
||||
Sub(triggers)
|
||||
@@ -39,7 +45,7 @@ Public Class frmFinalizePDF
|
||||
End Sub)
|
||||
opt.AddDbContext(
|
||||
Sub(options)
|
||||
options.UseSqlServer(CONNECTIONSTRING) _
|
||||
options.UseSqlServer(dCnnStr) _
|
||||
.EnableSensitiveDataLogging() _
|
||||
.EnableDetailedErrors()
|
||||
End Sub)
|
||||
@@ -95,8 +101,9 @@ Public Class frmFinalizePDF
|
||||
Select(Function(r As DataRow) r.Item("VALUE").ToString()).
|
||||
ToList()
|
||||
|
||||
Dim oBuffer As Byte() = ReadEnvelope(CInt(txtEnvelope.Text))
|
||||
Dim oNewBuffer = PDFBurner.BurnInstantJSONAnnotationsToPDF(oBuffer, oJsonList)
|
||||
Dim envelopeId As Integer = CInt(txtEnvelope.Text)
|
||||
Dim oBuffer As Byte() = ReadEnvelope(envelopeId)
|
||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
||||
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}R{txtReceiver.Text}.burned.pdf")
|
||||
|
||||
@@ -104,7 +111,15 @@ Public Class frmFinalizePDF
|
||||
|
||||
Process.Start(oNewPath)
|
||||
Catch ex As Exception
|
||||
MsgBox(ex.Message, MsgBoxStyle.Critical)
|
||||
Dim exMsg As StringBuilder = New StringBuilder(ex.Message).AppendLine()
|
||||
|
||||
Dim innerEx = ex.InnerException
|
||||
While (innerEx IsNot Nothing)
|
||||
exMsg.AppendLine(innerEx.Message)
|
||||
innerEx = innerEx.InnerException
|
||||
End While
|
||||
|
||||
MsgBox(exMsg.ToString(), MsgBoxStyle.Critical)
|
||||
End Try
|
||||
|
||||
End Sub
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
|
||||
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Controls.DocumentViewer" version="1.9.8" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.3.5" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.1.1" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Database" version="2.3.5.4" targetFramework="net462" />
|
||||
|
||||
@@ -72,11 +72,12 @@
|
||||
<Reference Include="DevExpress.XtraEditors.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a" />
|
||||
<Reference Include="DevExpress.XtraGauges.v21.2.Core, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
|
||||
<Reference Include="DevExpress.XtraReports.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.3.5.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.3.5\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.1.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.1.1\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Modules.Base, Version=1.3.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Modules.Base.1.3.8\lib\net462\DigitalData.Modules.Base.dll</HintPath>
|
||||
|
||||
@@ -14,6 +14,7 @@ Imports EnvelopeGenerator.Domain.Entities
|
||||
Imports DigitalData.Core.Abstraction.Application
|
||||
Imports EnvelopeGenerator.Infrastructure
|
||||
Imports Microsoft.EntityFrameworkCore
|
||||
Imports DigitalData.Core.Abstractions
|
||||
|
||||
Namespace Jobs
|
||||
Public Class FinalizeDocumentJob
|
||||
@@ -69,7 +70,9 @@ Namespace Jobs
|
||||
Database = New MSSQLServer(LogConfig, MSSQLServer.DecryptConnectionString(oConnectionString))
|
||||
|
||||
#Disable Warning BC40000 ' Type or member is obsolete
|
||||
Factory.Shared.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Factory.Shared _
|
||||
.BehaveOnPostBuild(PostBuildBehavior.Ignore) _
|
||||
.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Sub(opt)
|
||||
opt.AddDbTriggerParams(
|
||||
Sub(triggers)
|
||||
@@ -403,7 +406,6 @@ Namespace Jobs
|
||||
ParentFolderUID = pEnvelopeData.EnvelopeUUID
|
||||
End If
|
||||
|
||||
|
||||
Logger.Info("ParentFolderUID: [{0}]", ParentFolderUID)
|
||||
Dim oInputDocumentBuffer As Byte()
|
||||
If Not IsNothing(pEnvelopeData.DocAsByte) Then
|
||||
@@ -416,11 +418,11 @@ Namespace Jobs
|
||||
End Try
|
||||
End If
|
||||
|
||||
Return PDFBurner.BurnInstantJSONAnnotationsToPDF(oInputDocumentBuffer, oAnnotations)
|
||||
Return PDFBurner.BurnAnnotsToPDF(oInputDocumentBuffer, oAnnotations, pEnvelopeData.EnvelopeId)
|
||||
End Function
|
||||
|
||||
Private Function GetEnvelopeData(pEnvelopeId As Integer) As EnvelopeData
|
||||
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID,T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
|
||||
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID, T.ENVELOPE_TYPE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
|
||||
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
|
||||
WHERE T.GUID = {pEnvelopeId}"
|
||||
Dim oTable As DataTable = Database.GetDatatable(oSql)
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
Imports System.Drawing
|
||||
Imports System.Collections.Immutable
|
||||
Imports System.Drawing
|
||||
Imports System.IO
|
||||
Imports DevExpress.DataProcessing
|
||||
Imports DigitalData.Core.Abstraction.Application.Repository
|
||||
Imports DigitalData.Core.Abstractions
|
||||
Imports DigitalData.Modules.Base
|
||||
Imports DigitalData.Modules.Logging
|
||||
Imports GdPicture14
|
||||
Imports Newtonsoft.Json
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions
|
||||
Imports DevExpress.DataProcessing
|
||||
Imports EnvelopeGenerator.Domain.Entities
|
||||
Imports EnvelopeGenerator.PdfEditor
|
||||
Imports GdPicture14
|
||||
Imports Microsoft.EntityFrameworkCore
|
||||
Imports Newtonsoft.Json
|
||||
|
||||
Namespace Jobs.FinalizeDocument
|
||||
Public Class PDFBurner
|
||||
@@ -14,9 +20,6 @@ Namespace Jobs.FinalizeDocument
|
||||
Private ReadOnly Manager As AnnotationManager
|
||||
Private ReadOnly LicenseManager As LicenseManager
|
||||
|
||||
Private Const ANNOTATION_TYPE_IMAGE = "pspdfkit/image"
|
||||
Private Const ANNOTATION_TYPE_INK = "pspdfkit/ink"
|
||||
Private Const ANNOTATION_TYPE_WIDGET = "pspdfkit/widget"
|
||||
Private Property _pdfBurnerParams As PDFBurnerParams
|
||||
|
||||
Public Sub New(pLogConfig As LogConfig, pGDPictureLicenseKey As String, pdfBurnerParams As PDFBurnerParams)
|
||||
@@ -30,7 +33,103 @@ Namespace Jobs.FinalizeDocument
|
||||
_pdfBurnerParams = pdfBurnerParams
|
||||
End Sub
|
||||
|
||||
Public Function BurnInstantJSONAnnotationsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String)) As Byte()
|
||||
#Region "Burn PDF"
|
||||
Public Function BurnAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String), envelopeId As Integer) As Byte()
|
||||
'read the elements of envelope with their annotations
|
||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||
|
||||
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
||||
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
||||
If envelope Is Nothing Then
|
||||
Throw New BurnAnnotationException($"Envelope with Id {envelopeId} not found.")
|
||||
ElseIf envelope.ReadOnly Then
|
||||
Return pSourceBuffer
|
||||
End If
|
||||
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
.ToList()
|
||||
|
||||
Return If(elements.Any(),
|
||||
BurnElementAnnotsToPDF(pSourceBuffer, elements),
|
||||
BurnInstantJSONAnnotsToPDF(pSourceBuffer, pInstantJSONList))
|
||||
End Using
|
||||
End Function
|
||||
|
||||
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of Signature)) As Byte()
|
||||
' Add background
|
||||
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
|
||||
'TODO: take the length from the largest y
|
||||
pSourceBuffer = doc.Background(elements, 1.9500000000000002 * 0.93, 2.52 * 0.67).ExportStream().ToArray()
|
||||
|
||||
Dim oResult As GdPictureStatus
|
||||
Using oSourceStream As New MemoryStream(pSourceBuffer)
|
||||
' Open PDF
|
||||
oResult = Manager.InitFromStream(oSourceStream)
|
||||
If oResult <> GdPictureStatus.OK Then
|
||||
Throw New BurnAnnotationException($"Could not open document for burning: [{oResult}]")
|
||||
End If
|
||||
|
||||
' Imported from background (add to configuration)
|
||||
Dim margin As Double = 0.2
|
||||
Dim inchFactor As Double = 72
|
||||
|
||||
' Y offset of form fields
|
||||
Dim keys = {"position", "city", "date"} ' add to configuration
|
||||
Dim unitYOffsets = 0.2
|
||||
Dim yOffsetsOfFF = keys.
|
||||
Select(Function(k, i) New With {Key .Key = k, Key .Value = unitYOffsets * i + 1}).
|
||||
ToDictionary(Function(x) x.Key, Function(x) x.Value)
|
||||
|
||||
'Add annotations
|
||||
For Each element In elements
|
||||
|
||||
Dim frameX = (element.Left - 0.7 - margin)
|
||||
|
||||
Dim frame = element.Annotations.FirstOrDefault(Function(a) a.Name = "frame")
|
||||
Dim frameY = element.Top - 0.5 - margin
|
||||
Dim frameYShift = frame.Y - frameY * inchFactor
|
||||
Dim frameXShift = frame.X - frameX * inchFactor
|
||||
|
||||
For Each annot In element.Annotations
|
||||
Dim yOffsetofFF As Double = If(yOffsetsOfFF.TryGetValue(annot.Name, yOffsetofFF), yOffsetofFF, 0)
|
||||
Dim y = frameY + yOffsetofFF
|
||||
|
||||
If annot.Type = AnnotationType.FormField Then
|
||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
|
||||
ElseIf annot.Type = AnnotationType.Image Then
|
||||
AddImageAnnotation(
|
||||
annot.X / inchFactor,
|
||||
If(annot.Name = "signature", (annot.Y - frameYShift) / inchFactor, y),
|
||||
annot.Width / inchFactor,
|
||||
annot.Height / inchFactor,
|
||||
element.Page,
|
||||
annot.Value
|
||||
)
|
||||
ElseIf annot.Type = AnnotationType.Ink Then
|
||||
AddInkAnnotation(element.Page, annot.Value)
|
||||
End If
|
||||
Next
|
||||
Next
|
||||
|
||||
'Save PDF
|
||||
Using oNewStream As New MemoryStream()
|
||||
oResult = Manager.SaveDocumentToPDF(oNewStream)
|
||||
If oResult <> GdPictureStatus.OK Then
|
||||
Throw New BurnAnnotationException($"Could not save document to stream: [{oResult}]")
|
||||
End If
|
||||
|
||||
Manager.Close()
|
||||
|
||||
Return oNewStream.ToArray()
|
||||
End Using
|
||||
End Using
|
||||
End Using
|
||||
End Function
|
||||
|
||||
Public Function BurnInstantJSONAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String)) As Byte()
|
||||
Dim oResult As GdPictureStatus
|
||||
Using oSourceStream As New MemoryStream(pSourceBuffer)
|
||||
' Open PDF
|
||||
@@ -67,47 +166,42 @@ Namespace Jobs.FinalizeDocument
|
||||
End Using
|
||||
End Using
|
||||
End Function
|
||||
#End Region
|
||||
|
||||
#Region "Add Value"
|
||||
Private Sub AddInstantJSONAnnotationToPDF(pInstantJSON As String)
|
||||
Dim oAnnotationData = JsonConvert.DeserializeObject(Of AnnotationData)(pInstantJSON)
|
||||
|
||||
oAnnotationData.annotations.Reverse()
|
||||
|
||||
Dim yPosOfSigAnnot = oAnnotationData.annotations.ElementAt(2).bbox.ElementAt(1) - 71.84002685546875 + 7
|
||||
Dim isSeal = True 'First element is signature seal
|
||||
For Each oAnnotation In oAnnotationData.annotations
|
||||
Logger.Debug("Adding AnnotationID: " + oAnnotation.id)
|
||||
|
||||
Dim formFieldIndex = 0
|
||||
For Each oAnnotation In oAnnotationData.annotations
|
||||
Logger.Debug("Adding AnnotationID: " + oAnnotation.id)
|
||||
Select Case oAnnotation.type
|
||||
Case AnnotationType.Image
|
||||
AddImageAnnotation(oAnnotation, oAnnotationData.attachments)
|
||||
Exit Select
|
||||
Case AnnotationType.Ink
|
||||
AddInkAnnotation(oAnnotation)
|
||||
Exit Select
|
||||
Case AnnotationType.Widget
|
||||
'Add form field values
|
||||
Dim formFieldValue = oAnnotationData.formFieldValues.FirstOrDefault(Function(fv) fv.name = oAnnotation.id)
|
||||
If formFieldValue IsNot Nothing AndAlso Not _pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value) Then
|
||||
AddFormFieldValue(oAnnotation, formFieldValue)
|
||||
End If
|
||||
Exit Select
|
||||
End Select
|
||||
Next
|
||||
|
||||
Select Case oAnnotation.type
|
||||
Case ANNOTATION_TYPE_IMAGE
|
||||
|
||||
If (isSeal) Then
|
||||
oAnnotation.bbox.Item(1) = yPosOfSigAnnot
|
||||
End If
|
||||
|
||||
AddImageAnnotation(oAnnotation, oAnnotationData.attachments)
|
||||
Exit Select
|
||||
|
||||
Case ANNOTATION_TYPE_INK
|
||||
AddInkAnnotation(oAnnotation)
|
||||
Exit Select
|
||||
|
||||
Case ANNOTATION_TYPE_WIDGET
|
||||
'Add form field values
|
||||
Dim formFieldValue = oAnnotationData.formFieldValues.FirstOrDefault(Function(fv) fv.name = oAnnotation.id)
|
||||
If formFieldValue IsNot Nothing AndAlso Not _pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value) Then
|
||||
AddFormFieldValue(oAnnotation, formFieldValue, formFieldIndex)
|
||||
formFieldIndex += 1
|
||||
End If
|
||||
Exit Select
|
||||
End Select
|
||||
|
||||
isSeal = False
|
||||
Next
|
||||
End Sub
|
||||
|
||||
Private Function AddImageAnnotation(pAnnotation As Annotation, pAttachments As Dictionary(Of String, Attachment)) As Void
|
||||
Private Sub AddImageAnnotation(x As Double, y As Double, width As Double, height As Double, page As Integer, base64 As String)
|
||||
Manager.SelectPage(page)
|
||||
Manager.AddEmbeddedImageAnnotFromBase64(base64, x, y, width, height)
|
||||
End Sub
|
||||
|
||||
Private Sub AddImageAnnotation(pAnnotation As Annotation, pAttachments As Dictionary(Of String, Attachment))
|
||||
Dim oAttachment = pAttachments.Where(Function(a) a.Key = pAnnotation.imageAttachmentId).
|
||||
SingleOrDefault()
|
||||
|
||||
@@ -121,9 +215,26 @@ Namespace Jobs.FinalizeDocument
|
||||
|
||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||
Manager.AddEmbeddedImageAnnotFromBase64(oAttachment.Value.binary, oX, oY, oWidth, oHeight)
|
||||
End Function
|
||||
End Sub
|
||||
|
||||
Private Function AddInkAnnotation(pAnnotation As Annotation) As Void
|
||||
Private Sub AddInkAnnotation(page As Integer, value As String)
|
||||
|
||||
Dim ink = JsonConvert.DeserializeObject(Of Ink)(value)
|
||||
|
||||
Dim oSegments = ink.lines.points
|
||||
Dim oColor = ColorTranslator.FromHtml(ink.strokeColor)
|
||||
Manager.SelectPage(page)
|
||||
|
||||
For Each oSegment As List(Of List(Of Single)) In oSegments
|
||||
Dim oPoints = oSegment.
|
||||
Select(AddressOf ToPointF).
|
||||
ToArray()
|
||||
|
||||
Manager.AddFreeHandAnnot(oColor, oPoints)
|
||||
Next
|
||||
End Sub
|
||||
|
||||
Private Sub AddInkAnnotation(pAnnotation As Annotation)
|
||||
Dim oSegments = pAnnotation.lines.points
|
||||
Dim oColor = ColorTranslator.FromHtml(pAnnotation.strokeColor)
|
||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||
@@ -135,14 +246,29 @@ Namespace Jobs.FinalizeDocument
|
||||
|
||||
Manager.AddFreeHandAnnot(oColor, oPoints)
|
||||
Next
|
||||
End Function
|
||||
End Sub
|
||||
|
||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
|
||||
Manager.SelectPage(page)
|
||||
|
||||
' Add the text annotation
|
||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
|
||||
|
||||
' Set the font properties
|
||||
ant.FontName = _pdfBurnerParams.FontName
|
||||
ant.FontSize = _pdfBurnerParams.FontSize
|
||||
ant.FontStyle = _pdfBurnerParams.FontStyle
|
||||
Manager.SaveAnnotationsToPage()
|
||||
End Sub
|
||||
|
||||
Private Sub AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue)
|
||||
Dim ffIndex As Integer = EGName.Index(pAnnotation.egName)
|
||||
|
||||
Private Function AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue, index As Integer) As Void
|
||||
' Convert pixels to Inches
|
||||
Dim oBounds = pAnnotation.bbox.Select(AddressOf ToInches).ToList()
|
||||
|
||||
Dim oX = oBounds.Item(0)
|
||||
Dim oY = oBounds.Item(1) + _pdfBurnerParams.YOffset * index + _pdfBurnerParams.TopMargin
|
||||
Dim oY = oBounds.Item(1) + _pdfBurnerParams.YOffset * ffIndex + _pdfBurnerParams.TopMargin
|
||||
Dim oWidth = oBounds.Item(2)
|
||||
Dim oHeight = oBounds.Item(3)
|
||||
|
||||
@@ -155,8 +281,10 @@ Namespace Jobs.FinalizeDocument
|
||||
ant.FontSize = _pdfBurnerParams.FontSize
|
||||
ant.FontStyle = _pdfBurnerParams.FontStyle
|
||||
Manager.SaveAnnotationsToPage()
|
||||
End Function
|
||||
End Sub
|
||||
#End Region
|
||||
|
||||
#Region "Helpers"
|
||||
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||
@@ -169,22 +297,34 @@ Namespace Jobs.FinalizeDocument
|
||||
Private Function ToInches(pValue As Single) As Single
|
||||
Return pValue / 72
|
||||
End Function
|
||||
#End Region
|
||||
|
||||
#Region "Model"
|
||||
Friend Class AnnotationType
|
||||
Public Const Image As String = "pspdfkit/image"
|
||||
Public Const Ink As String = "pspdfkit/ink"
|
||||
Public Const Widget As String = "pspdfkit/widget"
|
||||
Public Const FormField As String = "pspdfkit/form-field-value"
|
||||
End Class
|
||||
|
||||
Friend Class AnnotationData
|
||||
Public Property annotations As List(Of Annotation)
|
||||
|
||||
Public ReadOnly Property SignatureAnnotations As List(Of List(Of Annotation))
|
||||
Public ReadOnly Property AnnotationsByReceiver As IEnumerable(Of List(Of Annotation))
|
||||
Get
|
||||
Dim result As New List(Of List(Of Annotation))()
|
||||
Return annotations _
|
||||
.Where(Function(annot) annot.hasStructuredID).ToList() _
|
||||
.GroupBy(Function(a) a.receiverId) _
|
||||
.Select(Function(g) g.ToList())
|
||||
End Get
|
||||
End Property
|
||||
|
||||
If annotations IsNot Nothing AndAlso annotations.Count > 0 Then
|
||||
For i As Integer = 0 To annotations.Count - 1 Step 6
|
||||
Dim group As List(Of Annotation) = annotations.Skip(i).Take(6).ToList()
|
||||
result.Add(group)
|
||||
Next
|
||||
End If
|
||||
|
||||
Return result
|
||||
Public ReadOnly Property UnstructuredAnnotations As IEnumerable(Of List(Of Annotation))
|
||||
Get
|
||||
Return annotations _
|
||||
.Where(Function(annot) Not annot.hasStructuredID).ToList() _
|
||||
.GroupBy(Function(a) a.receiverId) _
|
||||
.Select(Function(g) g.ToList())
|
||||
End Get
|
||||
End Property
|
||||
|
||||
@@ -193,16 +333,102 @@ Namespace Jobs.FinalizeDocument
|
||||
End Class
|
||||
|
||||
Friend Class Annotation
|
||||
|
||||
Private _id As String = Nothing
|
||||
|
||||
Public envelopeId As Integer = Nothing
|
||||
|
||||
Public receiverId As Integer = Nothing
|
||||
|
||||
Public index As Integer = Nothing
|
||||
|
||||
Public egName As String = PDFBurner.EGName.NoName
|
||||
|
||||
Public hasStructuredID As Boolean = False
|
||||
|
||||
Public ReadOnly Property isLabel As Boolean
|
||||
Get
|
||||
If String.IsNullOrEmpty(egName) Then
|
||||
Return False
|
||||
End If
|
||||
|
||||
Dim parts As String() = egName.Split("_"c)
|
||||
Return parts.Length > 1 AndAlso parts(1) = "label"
|
||||
End Get
|
||||
End Property
|
||||
|
||||
Public Property id As String
|
||||
Get
|
||||
Return _id
|
||||
End Get
|
||||
Set(value As String)
|
||||
_id = value
|
||||
|
||||
If String.IsNullOrWhiteSpace(_id) Then
|
||||
Throw New BurnAnnotationException("The identifier of annotation is null or empty.")
|
||||
End If
|
||||
|
||||
Dim parts As String() = value.Split("#"c)
|
||||
|
||||
If (parts.Length <> 4) Then
|
||||
Return
|
||||
'Throw New BurnAnnotationException($"The identifier of annotation has more or less than 4 sub-part. Id: {_id}")
|
||||
End If
|
||||
|
||||
If Not Integer.TryParse(parts(0), envelopeId) Then
|
||||
Throw New BurnAnnotationException($"The envelope ID of annotation is not integer. Id: {_id}")
|
||||
End If
|
||||
|
||||
If Not Integer.TryParse(parts(1), receiverId) Then
|
||||
Throw New BurnAnnotationException($"The receiver ID of annotation is not integer. Id: {_id}")
|
||||
End If
|
||||
|
||||
If Not Integer.TryParse(parts(2), index) Then
|
||||
Throw New BurnAnnotationException($"The index of annotation is not integer. Id: {_id}")
|
||||
End If
|
||||
|
||||
egName = parts(3)
|
||||
|
||||
hasStructuredID = True
|
||||
End Set
|
||||
End Property
|
||||
|
||||
Public Property bbox As List(Of Double)
|
||||
|
||||
Public Property type As String
|
||||
|
||||
Public Property isSignature As Boolean
|
||||
|
||||
Public Property imageAttachmentId As String
|
||||
|
||||
Public Property lines As Lines
|
||||
|
||||
Public Property pageIndex As Integer
|
||||
|
||||
Public Property strokeColor As String
|
||||
End Class
|
||||
|
||||
Friend Class Ink
|
||||
Public Property lines As Lines
|
||||
|
||||
Public Property strokeColor As String
|
||||
End Class
|
||||
|
||||
Public Class EGName
|
||||
Public Shared ReadOnly NoName As String = Guid.NewGuid().ToString()
|
||||
|
||||
Public Shared ReadOnly Seal As String = "signature"
|
||||
|
||||
Public Shared ReadOnly Index As ImmutableDictionary(Of String, Integer) =
|
||||
New Dictionary(Of String, Integer) From {
|
||||
{NoName, 0},
|
||||
{Seal, 0},
|
||||
{"position", 1},
|
||||
{"city", 2},
|
||||
{"date", 3}
|
||||
}.ToImmutableDictionary()
|
||||
End Class
|
||||
|
||||
Friend Class Lines
|
||||
Public Property points As List(Of List(Of List(Of Single)))
|
||||
End Class
|
||||
@@ -216,5 +442,6 @@ Namespace Jobs.FinalizeDocument
|
||||
Public Property name As String
|
||||
Public Property value As String
|
||||
End Class
|
||||
#End Region
|
||||
End Class
|
||||
End Namespace
|
||||
End Namespace
|
||||
@@ -2,8 +2,8 @@
|
||||
<packages>
|
||||
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
|
||||
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.3.5" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.1.1" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Database" version="2.3.5.4" targetFramework="net462" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
#if NETFRAMEWORK
|
||||
using System.Drawing;
|
||||
@@ -14,7 +15,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_ENVELOPE_DOCUMENT", Schema = "dbo")]
|
||||
public class Document
|
||||
public class Document : IHasEnvelope
|
||||
{
|
||||
public Document()
|
||||
{
|
||||
@@ -30,27 +31,50 @@ public class Document
|
||||
|
||||
[Required]
|
||||
[Column("ENVELOPE_ID")]
|
||||
public int EnvelopeId { get; set; } = 0;
|
||||
public int EnvelopeId { get; set; }
|
||||
#if NETFRAMEWORK
|
||||
= 0;
|
||||
#endif
|
||||
|
||||
[Column("BYTE_DATA", TypeName = "varbinary(max)")]
|
||||
public byte[] ByteData { get; set; }
|
||||
public byte[]
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
ByteData { get; set; }
|
||||
|
||||
public List<Signature> Elements { get; set; }
|
||||
#region File
|
||||
[Column("FILENAME", TypeName = "nvarchar(256)")]
|
||||
public string Filename { get; set; }
|
||||
|
||||
// TODO: * Check the Form App and remove the default value
|
||||
[NotMapped]
|
||||
[Column("FILEPATH", TypeName = "nvarchar(256)")]
|
||||
public string Filepath { get; set; }
|
||||
|
||||
[Column("FILENAME_ORIGINAL", TypeName = "nvarchar(256)")]
|
||||
public string
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
FileNameOriginal { get; set; }
|
||||
#endregion
|
||||
|
||||
public virtual List<Signature>
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Elements { get; set; }
|
||||
|
||||
[ForeignKey("EnvelopeId")]
|
||||
public virtual Envelope
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Envelope { get; set; }
|
||||
|
||||
#if NETFRAMEWORK
|
||||
[NotMapped]
|
||||
public string FileNameOriginal { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public bool IsTempFile { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string Filename { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public Bitmap Thumbnail { get; set; }
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DigitalData.Core.Abstractions.Interfaces;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
@@ -14,7 +15,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_DOCUMENT_STATUS", Schema = "dbo")]
|
||||
public class DocumentStatus : IHasEnvelope, IHasReceiver
|
||||
public class DocumentStatus : IHasEnvelope, IHasReceiver, IEntity
|
||||
{
|
||||
public DocumentStatus()
|
||||
{
|
||||
|
||||
92
EnvelopeGenerator.Domain/Entities/ElementAnnotation.cs
Normal file
92
EnvelopeGenerator.Domain/Entities/ElementAnnotation.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Entities
|
||||
#if NET
|
||||
;
|
||||
#elif NETFRAMEWORK
|
||||
{
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION")]
|
||||
public class ElementAnnotation
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
[Column("GUID", TypeName = "bigint")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("ELEMENT_ID", TypeName = "int")]
|
||||
public int ElementId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("NAME", TypeName = "nvarchar(100)")]
|
||||
[StringLength(100)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("VALUE", TypeName = "nvarchar(max)")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("TYPE", TypeName = "nvarchar(50)")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[Column("POSITION_X", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
X { get; set; }
|
||||
|
||||
[Column("POSITION_Y", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Y { get; set; }
|
||||
|
||||
[Column("WIDTH", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Width { get; set; }
|
||||
|
||||
[Column("HEIGHT", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Height { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("ADDED_WHEN", TypeName = "datetime")]
|
||||
public DateTime AddedWhen { get; set; }
|
||||
|
||||
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
|
||||
[Column("CHANGED_WHO", TypeName = "nvarchar(100)")]
|
||||
[StringLength(100)]
|
||||
public string
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
ChangedWho { get; set; }
|
||||
|
||||
[ForeignKey("ElementId")]
|
||||
public virtual Signature
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Element { get; set; }
|
||||
}
|
||||
|
||||
#if NETFRAMEWORK
|
||||
}
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
@@ -107,6 +108,10 @@ public class Envelope
|
||||
[Column("ENVELOPE_TYPE")]
|
||||
public int? EnvelopeTypeId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public bool ReadOnly => EnvelopeTypeId == 2;
|
||||
|
||||
[Column("CERTIFICATION_TYPE")]
|
||||
public int? CertificationType { get; set; }
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using DigitalData.Core.Abstractions.Interfaces;
|
||||
|
||||
|
||||
#if NETFRAMEWORK
|
||||
@@ -17,7 +18,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_ENVELOPE_HISTORY", Schema = "dbo")]
|
||||
public class History : IHasEnvelope, IHasReceiver
|
||||
public class History : IHasEnvelope, IHasReceiver, IEntity
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Entities
|
||||
@@ -14,7 +15,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
||||
public class Signature : ISignature
|
||||
public class Signature : ISignature, IHasReceiver
|
||||
{
|
||||
public Signature()
|
||||
{
|
||||
@@ -107,7 +108,17 @@ public class Signature : ISignature
|
||||
public virtual Document Document { get; set; }
|
||||
|
||||
[ForeignKey("ReceiverId")]
|
||||
public virtual Receiver Receiver { get; set; }
|
||||
public virtual Receiver
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Receiver { get; set; }
|
||||
|
||||
public virtual IEnumerable<ElementAnnotation>
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Annotations { get; set; }
|
||||
|
||||
#if NETFRAMEWORK
|
||||
[NotMapped]
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction.Attributes" Version="1.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="UserManager.Domain" Version="3.2.3" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.1.1" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Identity.Client" publicKeyToken="0a613f4dd989e8ae" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.55.0.0" newVersion="4.55.0.0" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.0.8.0" newVersion="3.0.8.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.IdentityModel.JsonWebTokens" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.GeneratorAPI.Extensions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages annotations and signature lifecycle for envelopes.
|
||||
/// </summary>
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AnnotationController : ControllerBase
|
||||
{
|
||||
[Obsolete("Use MediatR")]
|
||||
private readonly IEnvelopeHistoryService _historyService;
|
||||
|
||||
[Obsolete("Use MediatR")]
|
||||
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
private readonly ILogger<AnnotationController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="AnnotationController"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Use MediatR")]
|
||||
public AnnotationController(
|
||||
ILogger<AnnotationController> logger,
|
||||
IEnvelopeHistoryService envelopeHistoryService,
|
||||
IEnvelopeReceiverService envelopeReceiverService,
|
||||
IMediator mediator)
|
||||
{
|
||||
_historyService = envelopeHistoryService;
|
||||
_envelopeReceiverService = envelopeReceiverService;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates annotations for the authenticated envelope receiver.
|
||||
/// </summary>
|
||||
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[HttpPost]
|
||||
[Obsolete("This endpoint is for PSPDF Kit.")]
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||
{
|
||||
var signature = User.GetAuthReceiverSignature();
|
||||
var uuid = User.GetAuthEnvelopeUuid();
|
||||
|
||||
if (signature is null || uuid is null)
|
||||
{
|
||||
_logger.LogError("Authorization failed: authenticated user does not have a valid signature or envelope UUID.");
|
||||
return Unauthorized("User authentication is incomplete. Missing required claims for processing this request.");
|
||||
}
|
||||
|
||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
||||
|
||||
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
|
||||
return BadRequest();
|
||||
|
||||
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status409Conflict);
|
||||
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||
|
||||
var docSignedNotification = await _mediator
|
||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
|
||||
await _mediator.PublishSafely(docSignedNotification, cancel);
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rejects the document for the current receiver.
|
||||
/// </summary>
|
||||
/// <param name="reason">Optional rejection reason.</param>
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[HttpPost("reject")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||
{
|
||||
var signature = User.GetAuthReceiverSignature();
|
||||
var uuid = User.GetAuthEnvelopeUuid();
|
||||
var mail = User.GetAuthReceiverMail();
|
||||
if (uuid is null || signature is null || mail is null)
|
||||
{
|
||||
_logger.LogEnvelopeError(uuid: uuid, signature: signature,
|
||||
message: @$"Unauthorized POST request in api\\envelope\\reject. One of claims, Envelope, signature or mail ({mail}) is null.");
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||
|
||||
if (envRcvRes.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(envRcvRes.Notices);
|
||||
return Unauthorized("you are not authorized");
|
||||
}
|
||||
|
||||
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
|
||||
if (histRes.IsSuccess)
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
|
||||
_logger.LogNotice(histRes.Notices);
|
||||
return StatusCode(500, histRes.Messages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes configuration data required by the client applications.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of <see cref="ConfigController"/>.
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||
{
|
||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||
|
||||
/// <summary>
|
||||
/// Returns annotation configuration that was previously rendered by MVC.
|
||||
/// </summary>
|
||||
[HttpGet("Annotations")]
|
||||
public IActionResult GetAnnotationParams()
|
||||
{
|
||||
return Ok(_annotationParams.AnnotationJSObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to envelope documents for authenticated receivers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="DocumentController"/> class.
|
||||
/// </remarks>
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DocumentController(IMediator mediator, ILogger<DocumentController> logger) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the document bytes for the specified envelope receiver key.
|
||||
/// </summary>
|
||||
/// <param name="query">Encoded envelope key.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetDocument(ReadEnvelopeReceiverQuery query, CancellationToken cancel)
|
||||
{
|
||||
var envRcv = await mediator.Send(query, cancel).FirstAsync(Exceptions.NotFound);
|
||||
|
||||
var byteData = envRcv.Envelope?.Documents?.FirstOrDefault()?.ByteData;
|
||||
|
||||
if (byteData is null || byteData.Length == 0)
|
||||
{
|
||||
logger.LogError("Document byte data is null or empty for envelope-receiver entity:\n{envelopeKey}.",
|
||||
envRcv.ToJson(Format.Json.ForDiagnostics));
|
||||
throw new NotFoundException("Document is empty.");
|
||||
}
|
||||
|
||||
return File(byteData, "application/octet-stream");
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MediatR;
|
||||
using System.Threading.Tasks;
|
||||
using DigitalData.UserManager.Application.Services;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
@@ -23,12 +23,9 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
[Authorize]
|
||||
public class EmailTemplateController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EmailTemplateController> _logger;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
[Obsolete("Use IRepository")]
|
||||
private readonly IEmailTemplateRepository _repository;
|
||||
private readonly IRepository<EmailTemplate> _repository;
|
||||
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -39,12 +36,11 @@ public class EmailTemplateController : ControllerBase
|
||||
/// <param name="repository">
|
||||
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
|
||||
/// </param>
|
||||
[Obsolete("Use IRepository")]
|
||||
public EmailTemplateController(IMapper mapper, IEmailTemplateRepository repository, ILogger<EmailTemplateController> logger, IMediator mediator)
|
||||
[Obsolete("Use MediatR")]
|
||||
public EmailTemplateController(IMapper mapper, IRepository<EmailTemplate> repository, IMediator mediator)
|
||||
{
|
||||
_mapper = mapper;
|
||||
_repository = repository;
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
@@ -67,7 +63,7 @@ public class EmailTemplateController : ControllerBase
|
||||
{
|
||||
if (emailTemplate is null || (emailTemplate.Id is null && emailTemplate.Type is null))
|
||||
{
|
||||
var temps = await _repository.ReadAllAsync();
|
||||
var temps = await _repository.Query.ToListAsync();
|
||||
return Ok(_mapper.Map<IEnumerable<EmailTemplateDto>>(temps));
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
@@ -29,21 +27,16 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
public class EnvelopeController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EnvelopeController> _logger;
|
||||
[Obsolete("Use MediatR")]
|
||||
private readonly IEnvelopeService _envelopeService;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
||||
/// </summary>
|
||||
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
||||
/// <param name="envelopeService">Der Dienst, der für die Verarbeitung von Umschlägen zuständig ist.</param>
|
||||
/// <param name="mediator"></param>
|
||||
[Obsolete("Use MediatR")]
|
||||
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService, IMediator mediator)
|
||||
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_envelopeService = envelopeService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
@@ -59,98 +52,56 @@ public class EnvelopeController : ControllerBase
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||
{
|
||||
if (User.GetId() is int intId)
|
||||
return await _envelopeService.ReadByUserAsync(intId, min_status: envelope.Status?.Min, max_status: envelope.Status?.Max).ThenAsync(
|
||||
Success: envelopes =>
|
||||
{
|
||||
if (envelope.Id is int id)
|
||||
envelopes = envelopes.Where(e => e.Id == id);
|
||||
|
||||
if (envelope.Status is EnvelopeStatusQuery status)
|
||||
envelopes = envelopes.Where(e => e.Status == status);
|
||||
|
||||
if (envelope.Uuid is string uuid)
|
||||
envelopes = envelopes.Where(e => e.Uuid == uuid);
|
||||
|
||||
return envelopes.Any() ? Ok(envelopes) : NotFound();
|
||||
},
|
||||
Fail: IActionResult (msg, ntc) =>
|
||||
{
|
||||
_logger.LogNotice(ntc);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
});
|
||||
else
|
||||
{
|
||||
_logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
|
||||
return result.Any() ? Ok(result) : NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
|
||||
/// </summary>
|
||||
/// <param name="id">Die eindeutige ID des Umschlags.</param>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
|
||||
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
|
||||
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
|
||||
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[HttpGet("doc-result")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> GetDocResultAsync([FromQuery] int id, [FromQuery] bool view = false)
|
||||
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
||||
{
|
||||
if (User.GetId() is int intId)
|
||||
return await _envelopeService.ReadByUserAsync(intId).ThenAsync(
|
||||
Success: envelopes =>
|
||||
{
|
||||
var envelope = envelopes.Where(e => e.Id == id).FirstOrDefault();
|
||||
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
|
||||
var envelope = envelopes.FirstOrDefault();
|
||||
|
||||
if (envelope is null)
|
||||
return NotFound("Envelope not available.");
|
||||
else if (envelope?.DocResult is null)
|
||||
return NotFound("The document has not been fully signed or the result has not yet been released.");
|
||||
else
|
||||
{
|
||||
if (view)
|
||||
{
|
||||
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
|
||||
return File(envelope.DocResult, "application/pdf");
|
||||
}
|
||||
else
|
||||
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
|
||||
}
|
||||
},
|
||||
Fail: IActionResult (msg, ntc) =>
|
||||
{
|
||||
_logger.LogNotice(ntc);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
});
|
||||
else
|
||||
if (envelope is null)
|
||||
return NotFound("Envelope not available.");
|
||||
if (envelope.DocResult is null)
|
||||
return NotFound("The document has not been fully signed or the result has not yet been released.");
|
||||
|
||||
if (view)
|
||||
{
|
||||
_logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
|
||||
return File(envelope.DocResult, "application/pdf");
|
||||
}
|
||||
|
||||
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelope"></param>
|
||||
/// <param name="command"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromQuery] CreateEnvelopeCommand envelope)
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||
{
|
||||
envelope.UserId = User.GetId();
|
||||
var res = await _mediator.Send(envelope);
|
||||
var res = await _mediator.Send(command.Authorize(User.GetId()));
|
||||
|
||||
if (res is null)
|
||||
{
|
||||
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(envelope));
|
||||
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
@@ -11,12 +10,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Data;
|
||||
using System.Reflection.Metadata;
|
||||
using EnvelopeGenerator.Domain;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.Common.SQL;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
@@ -33,38 +28,19 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
public class EnvelopeReceiverController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EnvelopeReceiverController> _logger;
|
||||
|
||||
[Obsolete("Use MediatR")]
|
||||
private readonly IEnvelopeReceiverService _erService;
|
||||
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
private readonly IEnvelopeExecutor _envelopeExecutor;
|
||||
|
||||
private readonly IEnvelopeReceiverExecutor _erExecutor;
|
||||
|
||||
private readonly IDocumentExecutor _documentExecutor;
|
||||
|
||||
private readonly string _cnnStr;
|
||||
|
||||
/// <summary>
|
||||
/// Konstruktor für den EnvelopeReceiverController.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger-Instanz zur Protokollierung von Informationen und Fehlern.</param>
|
||||
/// <param name="envelopeReceiverService">Service zur Verwaltung von Umschlagempfängern.</param>
|
||||
/// <param name="mediator">Mediator-Instanz zur Verarbeitung von Befehlen und Abfragen.</param>
|
||||
/// <param name="mapper"></param>
|
||||
/// <param name="envelopeExecutor"></param>
|
||||
/// <param name="erExecutor"></param>
|
||||
/// <param name="documentExecutor"></param>
|
||||
/// <param name="csOpt"></param>
|
||||
[Obsolete("Use MediatR")]
|
||||
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
||||
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
||||
{
|
||||
_logger = logger;
|
||||
_erService = envelopeReceiverService;
|
||||
_mediator = mediator;
|
||||
_mapper = mapper;
|
||||
_envelopeExecutor = envelopeExecutor;
|
||||
@@ -87,7 +63,6 @@ public class EnvelopeReceiverController : ControllerBase
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
|
||||
{
|
||||
var username = User.GetUsernameOrDefault();
|
||||
@@ -99,20 +74,11 @@ public class EnvelopeReceiverController : ControllerBase
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
return await _erService.ReadByUsernameAsync(
|
||||
username: username,
|
||||
min_status: envelopeReceiver.Envelope.Status?.Min,
|
||||
max_status: envelopeReceiver.Envelope.Status?.Max,
|
||||
envelopeQuery: envelopeReceiver.Envelope,
|
||||
receiverQuery: envelopeReceiver.Receiver,
|
||||
ignore_statuses: envelopeReceiver.Envelope.Status?.Ignore ?? Array.Empty<EnvelopeStatus>())
|
||||
.ThenAsync(
|
||||
Success: Ok,
|
||||
Fail: IActionResult (msg, ntc) =>
|
||||
{
|
||||
_logger.LogNotice(ntc);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, msg);
|
||||
});
|
||||
envelopeReceiver = envelopeReceiver with { Username = username };
|
||||
|
||||
var result = await _mediator.Send(envelopeReceiver);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -129,25 +95,17 @@ public class EnvelopeReceiverController : ControllerBase
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[Authorize]
|
||||
[HttpGet("salute")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
||||
{
|
||||
return await _erService.ReadLastUsedReceiverNameByMailAsync(receiver.EmailAddress, receiver.Id, receiver.Signature).ThenAsync(
|
||||
Success: res => res is null ? NotFound() : Ok(res),
|
||||
Fail: IActionResult (msg, ntc) =>
|
||||
{
|
||||
if (ntc.HasFlag(Flag.NotFound))
|
||||
return NotFound();
|
||||
|
||||
_logger.LogNotice(ntc);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
});
|
||||
var name = await _mediator.Send(receiver);
|
||||
return name is null ? NotFound() : Ok(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns>HTTP-Antwort</returns>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
@@ -183,13 +141,10 @@ public class EnvelopeReceiverController : ControllerBase
|
||||
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request)
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||
{
|
||||
CancellationToken cancel = default;
|
||||
int userId = User.GetId();
|
||||
|
||||
#region Create Envelope
|
||||
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancel);
|
||||
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
||||
#endregion
|
||||
|
||||
#region Add receivers
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
@@ -13,36 +13,27 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
public class EnvelopeTypeController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EnvelopeTypeController> _logger;
|
||||
|
||||
[Obsolete("Use MediatR")]
|
||||
private readonly IEnvelopeTypeService _service;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="service"></param>
|
||||
[Obsolete("Use MediatR")]
|
||||
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IEnvelopeTypeService service)
|
||||
/// <param name="mediator"></param>
|
||||
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_service = service;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Use MediatR")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAllAsync()
|
||||
{
|
||||
return await _service.ReadAllAsync().ThenAsync(
|
||||
Success: Ok,
|
||||
Fail: IActionResult (msg, ntc) =>
|
||||
{
|
||||
_logger.LogNotice(ntc);
|
||||
return ntc.HasFlag(Flag.NotFound) ? NotFound() : StatusCode(StatusCodes.Status500InternalServerError);
|
||||
});
|
||||
var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.GeneratorAPI.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages read-only envelope sharing flows.
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class ReadOnlyController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<ReadOnlyController> _logger;
|
||||
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
|
||||
private readonly IEnvelopeMailService _mailService;
|
||||
private readonly IEnvelopeHistoryService _historyService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReadOnlyController"/> class.
|
||||
/// </summary>
|
||||
public ReadOnlyController(ILogger<ReadOnlyController> logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService)
|
||||
{
|
||||
_logger = logger;
|
||||
_readOnlyService = readOnlyService;
|
||||
_mailService = mailService;
|
||||
_historyService = historyService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new read-only receiver for the current envelope.
|
||||
/// </summary>
|
||||
/// <param name="createDto">Creation payload.</param>
|
||||
[HttpPost]
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||
{
|
||||
var authReceiverMail = User.GetAuthReceiverMail();
|
||||
if (authReceiverMail is null)
|
||||
{
|
||||
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var envelopeId = User.GetAuthEnvelopeId();
|
||||
if (envelopeId is null)
|
||||
{
|
||||
_logger.LogError("Envelope Id claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
createDto.AddedWho = authReceiverMail;
|
||||
createDto.EnvelopeId = envelopeId;
|
||||
|
||||
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
|
||||
|
||||
if (creationRes.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(creationRes);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id);
|
||||
if (readRes.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(creationRes);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
var newReadOnly = readRes.Data;
|
||||
|
||||
return await _mailService.SendAsync(newReadOnly).ThenAsync<int, IActionResult>(SuccessAsync: async _ =>
|
||||
{
|
||||
var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared);
|
||||
if (histRes.IsFailed)
|
||||
{
|
||||
_logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto));
|
||||
_logger.LogNotice(histRes.Notices);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
},
|
||||
|
||||
Fail: (msg, ntc) =>
|
||||
{
|
||||
_logger.LogNotice(ntc);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using DigitalData.Core.API;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using EnvelopeGenerator.Application.Receivers.Commands;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
@@ -14,22 +9,22 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
/// Controller für die Verwaltung von Empfängern.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dieser Controller bietet Endpunkte für CRUD-Operationen (Erstellen, Lesen, Aktualisieren, Löschen)
|
||||
/// sowie zusätzliche Funktionen wie das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
|
||||
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Obsolete("Use MediatR")]
|
||||
public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverService, CreateReceiverCommand, ReceiverDto, UpdateReceiverCommand, Receiver, int>
|
||||
public class ReceiverController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
|
||||
/// </summary>
|
||||
/// <param name="logger">Der Logger für die Protokollierung.</param>
|
||||
/// <param name="service">Der Dienst für Empfängeroperationen.</param>
|
||||
public ReceiverController(ILogger<ReceiverController> logger, IReceiverService service) : base(logger, service)
|
||||
/// <param name="mediator">Mediator für Anfragen.</param>
|
||||
public ReceiverController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,60 +35,13 @@ public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverS
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
||||
{
|
||||
if (receiver.Id is null && receiver.EmailAddress is null && receiver.Signature is null)
|
||||
return await base.GetAll();
|
||||
if (!receiver.HasAnyCriteria)
|
||||
{
|
||||
var all = await _mediator.Send(new ReadReceiverQuery());
|
||||
return Ok(all);
|
||||
}
|
||||
|
||||
if (receiver.Id is int id)
|
||||
return await _service.ReadByIdAsync(id).ThenAsync(
|
||||
Success: Ok,
|
||||
Fail: IActionResult (msg, ntc) =>
|
||||
{
|
||||
return NotFound();
|
||||
});
|
||||
|
||||
return await _service.ReadByAsync(emailAddress: receiver.EmailAddress, signature: receiver.Signature).ThenAsync(
|
||||
Success: Ok,
|
||||
Fail: IActionResult (msg, ntc) =>
|
||||
{
|
||||
return NotFound();
|
||||
});
|
||||
var result = await _mediator.Send(receiver);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
#region REMOVED ENDPOINTS
|
||||
/// <summary>
|
||||
/// Diese Methode ist deaktiviert und wird nicht verwendet.
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public override Task<IActionResult> GetAll() => base.GetAll();
|
||||
|
||||
/// <summary>
|
||||
/// Diese Methode ist deaktiviert und wird nicht verwendet.
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public override Task<IActionResult> Delete([FromRoute] int id) => base.Delete(id);
|
||||
|
||||
/// <summary>
|
||||
/// Diese Methode ist deaktiviert und wird nicht verwendet.
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public override Task<IActionResult> Update(UpdateReceiverCommand updateDto) => base.Update(updateDto);
|
||||
|
||||
/// <summary>
|
||||
/// Diese Methode ist deaktiviert und wird nicht verwendet.
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public override Task<IActionResult> Create(CreateReceiverCommand createDto)
|
||||
{
|
||||
return base.Create(createDto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diese Methode ist deaktiviert und wird nicht verwendet.
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public override Task<IActionResult> GetById([FromRoute] int id)
|
||||
{
|
||||
return base.GetById(id);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Resources;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.GeneratorAPI.Extensions;
|
||||
using EnvelopeGenerator.GeneratorAPI.Models;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/tfa")]
|
||||
public class TfaRegistrationController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TfaRegistrationController> _logger;
|
||||
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||
private readonly IAuthenticator _authenticator;
|
||||
private readonly IReceiverService _receiverService;
|
||||
private readonly TFARegParams _parameters;
|
||||
private readonly IStringLocalizer<Resource> _localizer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TfaRegistrationController"/> class.
|
||||
/// </summary>
|
||||
public TfaRegistrationController(
|
||||
ILogger<TfaRegistrationController> logger,
|
||||
IEnvelopeReceiverService envelopeReceiverService,
|
||||
IAuthenticator authenticator,
|
||||
IReceiverService receiverService,
|
||||
IOptions<TFARegParams> tfaRegParamsOptions,
|
||||
IStringLocalizer<Resource> localizer)
|
||||
{
|
||||
_logger = logger;
|
||||
_envelopeReceiverService = envelopeReceiverService;
|
||||
_authenticator = authenticator;
|
||||
_receiverService = receiverService;
|
||||
_parameters = tfaRegParamsOptions.Value;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates registration metadata (QR code and deadline) for a receiver.
|
||||
/// </summary>
|
||||
/// <param name="envelopeReceiverId">Encoded envelope receiver id.</param>
|
||||
[Authorize]
|
||||
[HttpGet("{envelopeReceiverId}")]
|
||||
public async Task<IActionResult> RegisterAsync(string envelopeReceiverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (uuid, signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
|
||||
|
||||
if (uuid is null || signature is null)
|
||||
{
|
||||
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
|
||||
return Unauthorized(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||
}
|
||||
|
||||
var secretResult = await _envelopeReceiverService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||
if (secretResult.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(secretResult.Notices);
|
||||
return NotFound(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||
}
|
||||
|
||||
var envelopeReceiver = secretResult.Data;
|
||||
|
||||
if (!envelopeReceiver.Envelope!.TFAEnabled)
|
||||
return Unauthorized(new { message = _localizer.WrongAccessCode() });
|
||||
|
||||
var receiver = envelopeReceiver.Receiver;
|
||||
receiver!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
|
||||
await _receiverService.UpdateAsync(receiver);
|
||||
var totpQr64 = _authenticator.GenerateTotpQrCode(userEmail: receiver.EmailAddress, secretKey: receiver.TotpSecretkey).ToBase64String();
|
||||
|
||||
if (receiver.TfaRegDeadline is null)
|
||||
{
|
||||
receiver.TfaRegDeadline = _parameters.Deadline;
|
||||
await _receiverService.UpdateAsync(receiver);
|
||||
}
|
||||
else if (receiver.TfaRegDeadline <= DateTime.Now)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status410Gone, new { message = _localizer.WrongAccessCode() });
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
envelopeReceiver.EnvelopeId,
|
||||
envelopeReceiver.Envelope!.Uuid,
|
||||
envelopeReceiver.Receiver!.Signature,
|
||||
receiver.TfaRegDeadline,
|
||||
TotpQR64 = totpQr64
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer.WrongEnvelopeReceiverId());
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs out the envelope receiver from cookie authentication.
|
||||
/// </summary>
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[HttpPost("auth/logout")]
|
||||
public async Task<IActionResult> LogOutAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{message}", ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||
}
|
||||
}
|
||||
}
|
||||
18
EnvelopeGenerator.GeneratorAPI/EnvelopeClaimTypes.cs
Normal file
18
EnvelopeGenerator.GeneratorAPI/EnvelopeClaimTypes.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides custom claim types for envelope-related information.
|
||||
/// </summary>
|
||||
public static class EnvelopeClaimTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Claim type for the title of an envelope.
|
||||
/// </summary>
|
||||
public static readonly string Title = $"Envelope{nameof(Title)}";
|
||||
|
||||
/// <summary>
|
||||
/// Claim type for the ID of an envelope.
|
||||
/// </summary>
|
||||
public static readonly string Id = $"Envelope{nameof(Id)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.Security.Claims;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for working with envelope-specific authentication claims.
|
||||
/// </summary>
|
||||
public static class EnvelopeAuthExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a claim value by type.
|
||||
/// </summary>
|
||||
/// <param name="user">The current claims principal.</param>
|
||||
/// <param name="claimType">The claim type to resolve.</param>
|
||||
/// <returns>The claim value or null when missing.</returns>
|
||||
public static string? GetClaimValue(this ClaimsPrincipal user, string claimType) => user.FindFirstValue(claimType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope UUID from the claims.
|
||||
/// </summary>
|
||||
public static string? GetAuthEnvelopeUuid(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver signature from the claims.
|
||||
/// </summary>
|
||||
public static string? GetAuthReceiverSignature(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Hash);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver display name from the claims.
|
||||
/// </summary>
|
||||
public static string? GetAuthReceiverName(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver email address from the claims.
|
||||
/// </summary>
|
||||
public static string? GetAuthReceiverMail(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Email);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope title from the claims.
|
||||
/// </summary>
|
||||
public static string? GetAuthEnvelopeTitle(this ClaimsPrincipal user) => user.FindFirstValue(EnvelopeClaimTypes.Title);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope identifier from the claims.
|
||||
/// </summary>
|
||||
public static int? GetAuthEnvelopeId(this ClaimsPrincipal user)
|
||||
{
|
||||
var envIdStr = user.FindFirstValue(EnvelopeClaimTypes.Id);
|
||||
return int.TryParse(envIdStr, out var envId) ? envId : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
|
||||
/// </summary>
|
||||
/// <param name="context">The current HTTP context.</param>
|
||||
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
|
||||
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
|
||||
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
|
||||
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
|
||||
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
|
||||
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
|
||||
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
|
||||
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
|
||||
new(ClaimTypes.Role, receiverRole)
|
||||
};
|
||||
|
||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
var authProperties = new AuthenticationProperties
|
||||
{
|
||||
AllowRefresh = false,
|
||||
IsPersistent = false
|
||||
};
|
||||
|
||||
await context.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(claimsIdentity),
|
||||
authProperties);
|
||||
}
|
||||
}
|
||||
14
EnvelopeGenerator.GeneratorAPI/Models/Auth.cs
Normal file
14
EnvelopeGenerator.GeneratorAPI/Models/Auth.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||
{
|
||||
public bool HasAccessCode => AccessCode is not null;
|
||||
|
||||
public bool HasSmsCode => SmsCode is not null;
|
||||
|
||||
public bool HasAuthenticatorCode => AuthenticatorCode is not null;
|
||||
|
||||
public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1;
|
||||
|
||||
public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode);
|
||||
}
|
||||
60
EnvelopeGenerator.GeneratorAPI/Models/ContactLink.cs
Normal file
60
EnvelopeGenerator.GeneratorAPI/Models/ContactLink.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a hyperlink for contact purposes with various HTML attributes.
|
||||
/// </summary>
|
||||
public class ContactLink
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the label of the hyperlink.
|
||||
/// </summary>
|
||||
public string Label { get; init; } = "Contact";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL that the hyperlink points to.
|
||||
/// </summary>
|
||||
public string Href { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target where the hyperlink should open.
|
||||
/// Commonly used values are "_blank", "_self", "_parent", "_top".
|
||||
/// </summary>
|
||||
public string Target { get; set; } = "_blank";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the relationship of the linked URL as space-separated link types.
|
||||
/// Examples include "nofollow", "noopener", "noreferrer".
|
||||
/// </summary>
|
||||
public string Rel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filename that should be downloaded when clicking the hyperlink.
|
||||
/// This attribute will only have an effect if the href attribute is set.
|
||||
/// </summary>
|
||||
public string Download { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language of the linked resource. Useful when linking to
|
||||
/// content in another language.
|
||||
/// </summary>
|
||||
public string HrefLang { get; set; } = "en";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type of the linked URL. Helps browsers to handle
|
||||
/// the type correctly when the link is clicked.
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional information about the hyperlink, typically viewed
|
||||
/// as a tooltip when the mouse hovers over the link.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an identifier for the hyperlink, unique within the HTML document.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
17
EnvelopeGenerator.GeneratorAPI/Models/Culture.cs
Normal file
17
EnvelopeGenerator.GeneratorAPI/Models/Culture.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
public class Culture
|
||||
{
|
||||
private string _language = string.Empty;
|
||||
public string Language { get => _language;
|
||||
init {
|
||||
_language = value;
|
||||
Info = new(value);
|
||||
}
|
||||
}
|
||||
public string FIClass { get; init; } = string.Empty;
|
||||
|
||||
public CultureInfo? Info { get; init; }
|
||||
}
|
||||
12
EnvelopeGenerator.GeneratorAPI/Models/Cultures.cs
Normal file
12
EnvelopeGenerator.GeneratorAPI/Models/Cultures.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
public class Cultures : List<Culture>
|
||||
{
|
||||
public IEnumerable<string> Languages => this.Select(c => c.Language);
|
||||
|
||||
public IEnumerable<string> FIClasses => this.Select(c => c.FIClass);
|
||||
|
||||
public Culture Default => this.First();
|
||||
|
||||
public Culture? this[string? language] => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
|
||||
}
|
||||
6
EnvelopeGenerator.GeneratorAPI/Models/CustomImages.cs
Normal file
6
EnvelopeGenerator.GeneratorAPI/Models/CustomImages.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
public class CustomImages : Dictionary<string, Image>
|
||||
{
|
||||
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
|
||||
}
|
||||
10
EnvelopeGenerator.GeneratorAPI/Models/ErrorViewModel.cs
Normal file
10
EnvelopeGenerator.GeneratorAPI/Models/ErrorViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
public class ErrorViewModel
|
||||
{
|
||||
public string Title { get; init; } = "404";
|
||||
|
||||
public string Subtitle { get; init; } = "Hmmm...";
|
||||
|
||||
public string Body { get; init; } = "It looks like one of the developers fell asleep";
|
||||
}
|
||||
10
EnvelopeGenerator.GeneratorAPI/Models/Image.cs
Normal file
10
EnvelopeGenerator.GeneratorAPI/Models/Image.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
public class Image
|
||||
{
|
||||
public string Src { get; init; } = string.Empty;
|
||||
|
||||
public Dictionary<string, string> Classes { get; init; } = new();
|
||||
|
||||
public string GetClassIn(string page) => Classes.TryGetValue(page, out var cls) && cls is not null ? cls : string.Empty;
|
||||
}
|
||||
6
EnvelopeGenerator.GeneratorAPI/Models/MainViewModel.cs
Normal file
6
EnvelopeGenerator.GeneratorAPI/Models/MainViewModel.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
public class MainViewModel
|
||||
{
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
|
||||
|
||||
public record Annotation : IAnnotation
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
|
||||
#region Bound Annotation
|
||||
[JsonIgnore]
|
||||
public string? HorBoundAnnotName { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string? VerBoundAnnotName { get; init; }
|
||||
#endregion
|
||||
|
||||
#region Layout
|
||||
[JsonIgnore]
|
||||
public double? MarginLeft { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double MarginLeftRatio { get; init; } = 1;
|
||||
|
||||
[JsonIgnore]
|
||||
public double? MarginTop { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double MarginTopRatio { get; init; } = 1;
|
||||
|
||||
public double? Width { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double WidthRatio { get; init; } = 1;
|
||||
|
||||
public double? Height { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double HeightRatio { get; init; } = 1;
|
||||
#endregion
|
||||
|
||||
#region Position
|
||||
public double Left => (MarginLeft ?? 0) + (HorBoundAnnot?.HorBoundary ?? 0);
|
||||
|
||||
public double Top => (MarginTop ?? 0) + (VerBoundAnnot?.VerBoundary ?? 0);
|
||||
#endregion
|
||||
|
||||
#region Boundary
|
||||
[JsonIgnore]
|
||||
public double HorBoundary => Left + (Width ?? 0);
|
||||
|
||||
[JsonIgnore]
|
||||
public double VerBoundary => Top + (Height ?? 0);
|
||||
#endregion
|
||||
|
||||
#region BoundAnnot
|
||||
[JsonIgnore]
|
||||
public Annotation? HorBoundAnnot { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Annotation? VerBoundAnnot { get; set; }
|
||||
#endregion
|
||||
|
||||
public Color? BackgroundColor { get; init; }
|
||||
|
||||
#region Border
|
||||
public Color? BorderColor { get; init; }
|
||||
|
||||
public string? BorderStyle { get; init; }
|
||||
|
||||
public int? BorderWidth { get; set; }
|
||||
#endregion
|
||||
|
||||
[JsonIgnore]
|
||||
internal Annotation Default
|
||||
{
|
||||
set
|
||||
{
|
||||
// To set null value, annotation must have null (0) value but null must has non-null value
|
||||
if (MarginLeft == null && value.MarginLeft != null)
|
||||
MarginLeft = value.MarginLeft * MarginLeftRatio;
|
||||
|
||||
if (MarginTop == null && value.MarginTop != null)
|
||||
MarginTop = value.MarginTop * MarginTopRatio;
|
||||
|
||||
if (Width == null && value.Width != null)
|
||||
Width = value.Width * WidthRatio;
|
||||
|
||||
if (Height == null && value.Height != null)
|
||||
Height = value.Height * HeightRatio;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
|
||||
|
||||
public class AnnotationParams
|
||||
{
|
||||
public AnnotationParams()
|
||||
{
|
||||
_AnnotationJSObjectInitor = new(CreateAnnotationJSObject);
|
||||
}
|
||||
|
||||
public Background? Background { get; init; }
|
||||
|
||||
#region Annotation
|
||||
[JsonIgnore]
|
||||
public Annotation? DefaultAnnotation { get; init; }
|
||||
|
||||
private readonly List<Annotation> _annots = new List<Annotation>();
|
||||
|
||||
public bool TryGet(string name, out Annotation annotation)
|
||||
{
|
||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||
annotation = _annots.FirstOrDefault(a => a.Name == name);
|
||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||
return annotation is not null;
|
||||
}
|
||||
|
||||
public required IEnumerable<Annotation> Annotations
|
||||
{
|
||||
get => _annots;
|
||||
init
|
||||
{
|
||||
_annots = value.ToList();
|
||||
|
||||
if (DefaultAnnotation is not null)
|
||||
foreach (var annot in _annots)
|
||||
annot.Default = DefaultAnnotation;
|
||||
|
||||
for (int i = 0; i < _annots.Count; i++)
|
||||
{
|
||||
#region set bound annotations
|
||||
// horizontal
|
||||
if (_annots[i].HorBoundAnnotName is string horBoundAnnotName)
|
||||
if (TryGet(horBoundAnnotName, out var horBoundAnnot))
|
||||
_annots[i].HorBoundAnnot = horBoundAnnot;
|
||||
else
|
||||
throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||
|
||||
// vertical
|
||||
if (_annots[i].VerBoundAnnotName is string verBoundAnnotName)
|
||||
if (TryGet(verBoundAnnotName, out var verBoundAnnot))
|
||||
_annots[i].VerBoundAnnot = verBoundAnnot;
|
||||
else
|
||||
throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AnnotationJSObject
|
||||
private Dictionary<string, IAnnotation> CreateAnnotationJSObject()
|
||||
{
|
||||
var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation);
|
||||
|
||||
if (Background is not null)
|
||||
{
|
||||
Background.Locate(_annots);
|
||||
dict.Add(Background.Name.ToLower(), Background);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
private readonly Lazy<Dictionary<string, IAnnotation>> _AnnotationJSObjectInitor;
|
||||
|
||||
public Dictionary<string, IAnnotation> AnnotationJSObject => _AnnotationJSObjectInitor.Value;
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
|
||||
|
||||
/// <summary>
|
||||
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
|
||||
/// It is only the first annotation as a background for other annotations.
|
||||
/// </summary>
|
||||
public record Background : IAnnotation
|
||||
{
|
||||
[JsonIgnore]
|
||||
public double Margin { get; init; }
|
||||
|
||||
public string Name { get; } = "Background";
|
||||
|
||||
public double? Width { get; set; }
|
||||
|
||||
public double? Height { get; set; }
|
||||
|
||||
public double Left { get; set; }
|
||||
|
||||
public double Top { get; set; }
|
||||
|
||||
public Color? BackgroundColor { get; init; }
|
||||
|
||||
#region Border
|
||||
public Color? BorderColor { get; init; }
|
||||
|
||||
public string? BorderStyle { get; init; }
|
||||
|
||||
public int? BorderWidth { get; set; }
|
||||
#endregion
|
||||
|
||||
public void Locate(IEnumerable<IAnnotation> annotations)
|
||||
{
|
||||
// set Top
|
||||
if (annotations.MinBy(a => a.Top)?.Top is double minTop)
|
||||
Top = minTop;
|
||||
|
||||
// set Left
|
||||
if (annotations.MinBy(a => a.Left)?.Left is double minLeft)
|
||||
Left = minLeft;
|
||||
|
||||
// set Width
|
||||
if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight)
|
||||
Width = maxRight - Left;
|
||||
|
||||
// set Height
|
||||
if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom)
|
||||
Height = maxBottom - Top;
|
||||
|
||||
// add margins
|
||||
Top -= Margin;
|
||||
Left -= Margin;
|
||||
Width += Margin * 2;
|
||||
Height += Margin * 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
|
||||
|
||||
public record Color
|
||||
{
|
||||
public int R { get; init; } = 0;
|
||||
|
||||
public int G { get; init; } = 0;
|
||||
|
||||
public int B { get; init; } = 0;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0;
|
||||
|
||||
public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
|
||||
|
||||
public interface IAnnotation
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
double? Width { get; }
|
||||
|
||||
double? Height { get; }
|
||||
|
||||
double Left { get; }
|
||||
|
||||
double Top { get; }
|
||||
|
||||
Color? BackgroundColor { get; }
|
||||
|
||||
Color? BorderColor { get; }
|
||||
|
||||
string? BorderStyle { get; }
|
||||
|
||||
int? BorderWidth { get; }
|
||||
}
|
||||
17
EnvelopeGenerator.GeneratorAPI/Models/TFARegParams.cs
Normal file
17
EnvelopeGenerator.GeneratorAPI/Models/TFARegParams.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the parameters for two-factor authentication (2FA) registration.
|
||||
/// </summary>
|
||||
public class TFARegParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum allowed time for completing the registration process.
|
||||
/// </summary>
|
||||
public TimeSpan TimeLimit { get; init; } = new(0, 30, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The deadline for registration, calculated as the current time plus the <see cref="TimeLimit"/>.
|
||||
/// </summary>
|
||||
public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks);
|
||||
}
|
||||
@@ -60,7 +60,8 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
services.AddDbRepository(opt =>
|
||||
{
|
||||
// scan EnvelopeGenerator
|
||||
opt.RegisterFromAssembly<EGDbContext>(typeof(Config).Assembly);
|
||||
opt.RegisterFromAssembly<EGDbContext>(typeof(EnvelopeReceiver).Assembly);
|
||||
|
||||
// scan UserManager
|
||||
opt.RegisterFromAssembly<EGDbContext>(typeof(User).Assembly);
|
||||
#if NET
|
||||
|
||||
@@ -11,7 +11,6 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using DigitalData.UserManager.Infrastructure;
|
||||
using DigitalData.UserManager.Infrastructure.Contracts;
|
||||
using DigitalData.Core.Client;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
#elif NETFRAMEWORK
|
||||
using System.Linq;
|
||||
#endif
|
||||
@@ -40,7 +39,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
, IUserManagerDbContext, IMailDbContext
|
||||
#endif
|
||||
{
|
||||
public DbSet<Domain.Entities.Config> Configs { get; set; }
|
||||
public DbSet<Config> Configs { get; set; }
|
||||
|
||||
public DbSet<EnvelopeReceiver> EnvelopeReceivers { get; set; }
|
||||
|
||||
@@ -98,27 +97,6 @@ public abstract class EGDbContextBase : DbContext
|
||||
{
|
||||
_triggers = triggerParamOptions.Value;
|
||||
_logger = logger;
|
||||
|
||||
Configs = Set<Config>();
|
||||
EnvelopeReceivers = Set<EnvelopeReceiver>();
|
||||
Envelopes = Set<Envelope>();
|
||||
DocumentReceiverElements = Set<Signature>();
|
||||
DocumentStatus = Set<DocumentStatus>();
|
||||
EnvelopeDocument = Set<Document>();
|
||||
EnvelopeHistories = Set<History>();
|
||||
EnvelopeTypes = Set<EnvelopeType>();
|
||||
Receivers = Set<Receiver>();
|
||||
|
||||
GroupOfUsers = Set<GroupOfUser>();
|
||||
Groups = Set<Group>();
|
||||
ModuleOfUsers = Set<ModuleOfUser>();
|
||||
Modules = Set<Module>();
|
||||
Users = Set<User>();
|
||||
UserReps = Set<UserRep>();
|
||||
#if NET
|
||||
EMailOuts = Set<EmailOut>();
|
||||
#endif
|
||||
EnvelopeReceiverReadOnlys = Set<EnvelopeReceiverReadOnly>();
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
@@ -147,7 +125,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
modelBuilder.Entity<Envelope>()
|
||||
.HasMany(e => e.Documents)
|
||||
.WithOne()
|
||||
.WithOne(d => d.Envelope)
|
||||
.HasForeignKey(ed => ed.EnvelopeId);
|
||||
|
||||
modelBuilder.Entity<Envelope>()
|
||||
@@ -215,6 +193,13 @@ public abstract class EGDbContextBase : DbContext
|
||||
.HasForeignKey(ds => ds.ReceiverId);
|
||||
#endregion DocumentStatus
|
||||
|
||||
#region Annotation
|
||||
modelBuilder.Entity<Signature>()
|
||||
.HasMany(signature => signature.Annotations)
|
||||
.WithOne(annot => annot.Element)
|
||||
.HasForeignKey(annot => annot.ElementId);
|
||||
#endregion
|
||||
|
||||
#region Trigger
|
||||
// Configure entities to handle database triggers
|
||||
void AddTrigger<T>() where T : class => _triggers
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
public EGDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory()) // Önemli!
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.migration.json")
|
||||
.Build();
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.3.5" />
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.3" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
22
EnvelopeGenerator.Jobs/EnvelopeGenerator.Jobs.csproj
Normal file
22
EnvelopeGenerator.Jobs/EnvelopeGenerator.Jobs.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Quartz" Version="3.9.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
151
EnvelopeGenerator.Jobs/Jobs/APIBackendJobs/APIEnvelopeJob.cs
Normal file
151
EnvelopeGenerator.Jobs/Jobs/APIBackendJobs/APIEnvelopeJob.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Quartz;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.APIBackendJobs;
|
||||
|
||||
public class APIEnvelopeJob(ILogger<APIEnvelopeJob>? logger = null) : IJob
|
||||
{
|
||||
private readonly ILogger<APIEnvelopeJob> _logger = logger ?? NullLogger<APIEnvelopeJob>.Instance;
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var jobId = context.JobDetail.Key.ToString();
|
||||
_logger.LogDebug("API Envelopes - Starting job {JobId}", jobId);
|
||||
|
||||
try
|
||||
{
|
||||
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
_logger.LogWarning("API Envelopes - Connection string missing");
|
||||
return;
|
||||
}
|
||||
|
||||
await using var connection = new SqlConnection(connectionString);
|
||||
await connection.OpenAsync(context.CancellationToken);
|
||||
|
||||
await ProcessInvitationsAsync(connection, context.CancellationToken);
|
||||
await ProcessWithdrawnAsync(connection, context.CancellationToken);
|
||||
|
||||
_logger.LogDebug("API Envelopes - Completed job {JobId} successfully", jobId);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "API Envelopes job failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogDebug("API Envelopes execution for {JobId} ended", jobId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessInvitationsAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE SOURCE = 'API' AND STATUS = 1003 ORDER BY GUID";
|
||||
var envelopeIds = new List<int>();
|
||||
|
||||
await using (var command = new SqlCommand(sql, connection))
|
||||
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
|
||||
{
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
if (reader[0] is int id)
|
||||
{
|
||||
envelopeIds.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (envelopeIds.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("SendInvMail - No envelopes found");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("SendInvMail - Found {Count} envelopes", envelopeIds.Count);
|
||||
var total = envelopeIds.Count;
|
||||
var current = 1;
|
||||
|
||||
foreach (var id in envelopeIds)
|
||||
{
|
||||
_logger.LogInformation("SendInvMail - Processing Envelope {EnvelopeId} ({Current}/{Total})", id, current, total);
|
||||
try
|
||||
{
|
||||
// Placeholder for invitation email sending logic.
|
||||
_logger.LogDebug("SendInvMail - Marking envelope {EnvelopeId} as queued", id);
|
||||
const string updateSql = "UPDATE TBSIG_ENVELOPE SET CURRENT_WORK_APP = @App WHERE GUID = @Id";
|
||||
await using var updateCommand = new SqlCommand(updateSql, connection);
|
||||
updateCommand.Parameters.AddWithValue("@App", "signFLOW_API_EnvJob_InvMail");
|
||||
updateCommand.Parameters.AddWithValue("@Id", id);
|
||||
await updateCommand.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "SendInvMail - Unhandled exception while working envelope {EnvelopeId}", id);
|
||||
}
|
||||
|
||||
current++;
|
||||
_logger.LogInformation("SendInvMail - Envelope finalized");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessWithdrawnAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = @"SELECT ENV.GUID, REJ.COMMENT AS REJECTION_REASON FROM
|
||||
(SELECT * FROM TBSIG_ENVELOPE WHERE STATUS = 1009 AND SOURCE = 'API') ENV INNER JOIN
|
||||
(SELECT MAX(GUID) GUID, ENVELOPE_ID, MAX(ADDED_WHEN) ADDED_WHEN, MAX(ACTION_DATE) ACTION_DATE, COMMENT FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 1009 GROUP BY ENVELOPE_ID, COMMENT ) REJ ON ENV.GUID = REJ.ENVELOPE_ID LEFT JOIN
|
||||
(SELECT * FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 3004 ) M_Send ON ENV.GUID = M_Send.ENVELOPE_ID
|
||||
WHERE M_Send.GUID IS NULL";
|
||||
|
||||
var withdrawn = new List<(int EnvelopeId, string Reason)>();
|
||||
await using (var command = new SqlCommand(sql, connection))
|
||||
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
|
||||
{
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var reason = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
|
||||
withdrawn.Add((id, reason));
|
||||
}
|
||||
}
|
||||
|
||||
if (withdrawn.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("WithdrawnEnv - No envelopes found");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("WithdrawnEnv - Found {Count} envelopes", withdrawn.Count);
|
||||
var total = withdrawn.Count;
|
||||
var current = 1;
|
||||
|
||||
foreach (var (envelopeId, reason) in withdrawn)
|
||||
{
|
||||
_logger.LogInformation("WithdrawnEnv - Processing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
|
||||
try
|
||||
{
|
||||
// Log withdrawn mail trigger placeholder
|
||||
const string insertHistory = "INSERT INTO TBSIG_ENVELOPE_HISTORY (ENVELOPE_ID, STATUS, USER_REFERENCE, ADDED_WHEN, ACTION_DATE, COMMENT) VALUES (@EnvelopeId, @Status, @UserReference, GETDATE(), GETDATE(), @Comment)";
|
||||
await using var insertCommand = new SqlCommand(insertHistory, connection);
|
||||
insertCommand.Parameters.AddWithValue("@EnvelopeId", envelopeId);
|
||||
insertCommand.Parameters.AddWithValue("@Status", 3004);
|
||||
insertCommand.Parameters.AddWithValue("@UserReference", "API");
|
||||
insertCommand.Parameters.AddWithValue("@Comment", reason ?? string.Empty);
|
||||
await insertCommand.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "WithdrawnEnv - Unhandled exception while working envelope {EnvelopeId}", envelopeId);
|
||||
}
|
||||
|
||||
current++;
|
||||
_logger.LogInformation("WithdrawnEnv - Envelope finalized");
|
||||
}
|
||||
}
|
||||
}
|
||||
30
EnvelopeGenerator.Jobs/Jobs/DataRowExtensions.cs
Normal file
30
EnvelopeGenerator.Jobs/Jobs/DataRowExtensions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs;
|
||||
|
||||
public static class DataRowExtensions
|
||||
{
|
||||
public static T? GetValueOrDefault<T>(this DataRow row, string columnName, T? defaultValue = default)
|
||||
{
|
||||
if (!row.Table.Columns.Contains(columnName))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
var value = row[columnName];
|
||||
if (value == DBNull.Value)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public static class FinalizeDocumentExceptions
|
||||
{
|
||||
public class MergeDocumentException : ApplicationException
|
||||
{
|
||||
public MergeDocumentException(string message) : base(message) { }
|
||||
public MergeDocumentException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class BurnAnnotationException : ApplicationException
|
||||
{
|
||||
public BurnAnnotationException(string message) : base(message) { }
|
||||
public BurnAnnotationException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class CreateReportException : ApplicationException
|
||||
{
|
||||
public CreateReportException(string message) : base(message) { }
|
||||
public CreateReportException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class ExportDocumentException : ApplicationException
|
||||
{
|
||||
public ExportDocumentException(string message) : base(message) { }
|
||||
public ExportDocumentException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Quartz;
|
||||
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class FinalizeDocumentJob : IJob
|
||||
{
|
||||
private readonly ILogger<FinalizeDocumentJob> _logger;
|
||||
private readonly PDFBurner _pdfBurner;
|
||||
private readonly PDFMerger _pdfMerger;
|
||||
private readonly ReportCreator _reportCreator;
|
||||
|
||||
private record ConfigSettings(string DocumentPath, string DocumentPathOrigin, string ExportPath);
|
||||
|
||||
public FinalizeDocumentJob(
|
||||
ILogger<FinalizeDocumentJob> logger,
|
||||
PDFBurner pdfBurner,
|
||||
PDFMerger pdfMerger,
|
||||
ReportCreator reportCreator)
|
||||
{
|
||||
_logger = logger;
|
||||
_pdfBurner = pdfBurner;
|
||||
_pdfMerger = pdfMerger;
|
||||
_reportCreator = reportCreator;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var jobId = context.JobDetail.Key.ToString();
|
||||
_logger.LogDebug("Starting job {JobId}", jobId);
|
||||
|
||||
try
|
||||
{
|
||||
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
_logger.LogWarning("FinalizeDocument - Connection string missing");
|
||||
return;
|
||||
}
|
||||
|
||||
await using var connection = new SqlConnection(connectionString);
|
||||
await connection.OpenAsync(context.CancellationToken);
|
||||
|
||||
var config = await LoadConfigurationAsync(connection, context.CancellationToken);
|
||||
var envelopes = await LoadCompletedEnvelopesAsync(connection, context.CancellationToken);
|
||||
|
||||
if (envelopes.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("No completed envelopes found");
|
||||
return;
|
||||
}
|
||||
|
||||
var total = envelopes.Count;
|
||||
var current = 1;
|
||||
|
||||
foreach (var envelopeId in envelopes)
|
||||
{
|
||||
_logger.LogInformation("Finalizing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
|
||||
try
|
||||
{
|
||||
var envelopeData = await GetEnvelopeDataAsync(connection, envelopeId, context.CancellationToken);
|
||||
if (envelopeData is null)
|
||||
{
|
||||
_logger.LogWarning("Envelope data not found for {EnvelopeId}", envelopeId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = envelopeData.Value;
|
||||
|
||||
var envelope = new Envelope
|
||||
{
|
||||
Id = envelopeId,
|
||||
Uuid = data.EnvelopeUuid ?? string.Empty,
|
||||
Title = data.Title ?? string.Empty,
|
||||
FinalEmailToCreator = (int)FinalEmailType.No,
|
||||
FinalEmailToReceivers = (int)FinalEmailType.No
|
||||
};
|
||||
|
||||
var burned = _pdfBurner.BurnAnnotsToPDF(data.DocumentBytes, data.AnnotationData, envelopeId);
|
||||
var report = _reportCreator.CreateReport(connection, envelope);
|
||||
var merged = _pdfMerger.MergeDocuments(burned, report);
|
||||
|
||||
var outputDirectory = Path.Combine(config.ExportPath, data.ParentFolderUid);
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
var outputPath = Path.Combine(outputDirectory, $"{envelope.Uuid}.pdf");
|
||||
await File.WriteAllBytesAsync(outputPath, merged, context.CancellationToken);
|
||||
|
||||
await UpdateDocumentResultAsync(connection, envelopeId, merged, context.CancellationToken);
|
||||
await ArchiveEnvelopeAsync(connection, envelopeId, context.CancellationToken);
|
||||
}
|
||||
catch (MergeDocumentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Certificate Document job failed at merging documents");
|
||||
}
|
||||
catch (ExportDocumentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Certificate Document job failed at exporting document");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception while working envelope {EnvelopeId}", envelopeId);
|
||||
}
|
||||
|
||||
current++;
|
||||
_logger.LogInformation("Envelope {EnvelopeId} finalized", envelopeId);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Completed job {JobId} successfully", jobId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Certificate Document job failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogDebug("Job execution for {JobId} ended", jobId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ConfigSettings> LoadConfigurationAsync(SqlConnection connection, CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = "SELECT TOP 1 DOCUMENT_PATH, EXPORT_PATH FROM TBSIG_CONFIG";
|
||||
await using var command = new SqlCommand(sql, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var documentPath = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
|
||||
var exportPath = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
|
||||
return new ConfigSettings(documentPath, documentPath, exportPath);
|
||||
}
|
||||
|
||||
return new ConfigSettings(string.Empty, string.Empty, Path.GetTempPath());
|
||||
}
|
||||
|
||||
private async Task<List<int>> LoadCompletedEnvelopesAsync(SqlConnection connection, CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE STATUS = @Status AND DATEDIFF(minute, CHANGED_WHEN, GETDATE()) >= 1 ORDER BY GUID";
|
||||
var ids = new List<int>();
|
||||
await using var command = new SqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeCompletelySigned);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
ids.Add(reader.GetInt32(0));
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private async Task<(int EnvelopeId, string? EnvelopeUuid, string? Title, byte[] DocumentBytes, List<string> AnnotationData, string ParentFolderUid)?> GetEnvelopeDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = @"SELECT T.GUID, T.ENVELOPE_UUID, T.TITLE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
|
||||
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
|
||||
WHERE T.GUID = @EnvelopeId";
|
||||
|
||||
await using var command = new SqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
|
||||
await using var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken);
|
||||
if (!await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var envelopeUuid = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
|
||||
var title = reader.IsDBNull(2) ? string.Empty : reader.GetString(2);
|
||||
var filePath = reader.IsDBNull(3) ? string.Empty : reader.GetString(3);
|
||||
var bytes = reader.IsDBNull(4) ? Array.Empty<byte>() : (byte[])reader[4];
|
||||
await reader.CloseAsync();
|
||||
|
||||
if (bytes.Length == 0 && !string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
|
||||
{
|
||||
bytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
|
||||
}
|
||||
|
||||
var annotations = await GetAnnotationDataAsync(connection, envelopeId, cancellationToken);
|
||||
|
||||
var parentFolderUid = !string.IsNullOrWhiteSpace(filePath)
|
||||
? Path.GetFileName(Path.GetDirectoryName(filePath) ?? string.Empty)
|
||||
: envelopeUuid;
|
||||
|
||||
return (envelopeId, envelopeUuid, title, bytes, annotations, parentFolderUid ?? envelopeUuid ?? envelopeId.ToString());
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetAnnotationDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = "SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = @EnvelopeId";
|
||||
var result = new List<string>();
|
||||
await using var command = new SqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
if (!reader.IsDBNull(0))
|
||||
{
|
||||
result.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task UpdateDocumentResultAsync(SqlConnection connection, int envelopeId, byte[] bytes, CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = "UPDATE TBSIG_ENVELOPE SET DOC_RESULT = @ImageData WHERE GUID = @EnvelopeId";
|
||||
await using var command = new SqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@ImageData", bytes);
|
||||
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task ArchiveEnvelopeAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = "UPDATE TBSIG_ENVELOPE SET STATUS = @Status, CHANGED_WHEN = GETDATE() WHERE GUID = @EnvelopeId";
|
||||
await using var command = new SqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeArchived);
|
||||
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
277
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFBurner.cs
Normal file
277
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFBurner.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using iText.IO.Image;
|
||||
using iText.Kernel.Colors;
|
||||
using iText.Kernel.Pdf;
|
||||
using iText.Kernel.Pdf.Canvas;
|
||||
using iText.Layout;
|
||||
using iText.Layout.Element;
|
||||
using iText.Layout.Font;
|
||||
using iText.Layout.Properties;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
using LayoutImage = iText.Layout.Element.Image;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class PDFBurner
|
||||
{
|
||||
private static readonly FontProvider FontProvider = CreateFontProvider();
|
||||
private readonly ILogger<PDFBurner> _logger;
|
||||
private readonly PDFBurnerParams _pdfBurnerParams;
|
||||
|
||||
public PDFBurner() : this(NullLogger<PDFBurner>.Instance, new PDFBurnerParams())
|
||||
{
|
||||
}
|
||||
|
||||
public PDFBurner(ILogger<PDFBurner> logger, PDFBurnerParams? pdfBurnerParams = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_pdfBurnerParams = pdfBurnerParams ?? new PDFBurnerParams();
|
||||
}
|
||||
|
||||
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, IList<string> instantJsonList, int envelopeId)
|
||||
{
|
||||
if (sourceBuffer is null || sourceBuffer.Length == 0)
|
||||
{
|
||||
throw new BurnAnnotationException("Source document is empty");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var inputStream = new MemoryStream(sourceBuffer);
|
||||
using var outputStream = new MemoryStream();
|
||||
using var reader = new PdfReader(inputStream);
|
||||
using var writer = new PdfWriter(outputStream);
|
||||
using var pdf = new PdfDocument(reader, writer);
|
||||
|
||||
foreach (var json in instantJsonList ?? Enumerable.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var annotationData = JsonConvert.DeserializeObject<AnnotationData>(json);
|
||||
if (annotationData?.annotations is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
annotationData.annotations.Reverse();
|
||||
|
||||
foreach (var annotation in annotationData.annotations)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (annotation.type)
|
||||
{
|
||||
case AnnotationType.Image:
|
||||
AddImageAnnotation(pdf, annotation, annotationData.attachments);
|
||||
break;
|
||||
case AnnotationType.Ink:
|
||||
AddInkAnnotation(pdf, annotation);
|
||||
break;
|
||||
case AnnotationType.Widget:
|
||||
var formFieldValue = annotationData.formFieldValues?.FirstOrDefault(fv => fv.name == annotation.id);
|
||||
if (formFieldValue is not null && !_pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value))
|
||||
{
|
||||
AddFormFieldValue(pdf, annotation, formFieldValue.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error applying annotation {AnnotationId} on envelope {EnvelopeId}", annotation.id, envelopeId);
|
||||
throw new BurnAnnotationException("Adding annotation failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pdf.Close();
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
catch (BurnAnnotationException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to burn annotations for envelope {EnvelopeId}", envelopeId);
|
||||
throw new BurnAnnotationException("Annotations could not be burned", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddImageAnnotation(PdfDocument pdf, Annotation annotation, Dictionary<string, Attachment>? attachments)
|
||||
{
|
||||
if (attachments is null || string.IsNullOrWhiteSpace(annotation.imageAttachmentId) || !attachments.TryGetValue(annotation.imageAttachmentId, out var attachment))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var page = pdf.GetPage(annotation.pageIndex + 1);
|
||||
var bounds = annotation.bbox.Select(ToInches).ToList();
|
||||
var x = (float)bounds[0];
|
||||
var y = (float)bounds[1];
|
||||
var width = (float)bounds[2];
|
||||
var height = (float)bounds[3];
|
||||
|
||||
var imageBytes = Convert.FromBase64String(attachment.binary);
|
||||
var imageData = ImageDataFactory.Create(imageBytes);
|
||||
var image = new LayoutImage(imageData)
|
||||
.ScaleAbsolute(width, height)
|
||||
.SetFixedPosition(annotation.pageIndex + 1, x, y);
|
||||
|
||||
using var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
|
||||
canvas.Add(image);
|
||||
}
|
||||
|
||||
private void AddInkAnnotation(PdfDocument pdf, Annotation annotation)
|
||||
{
|
||||
if (annotation.lines?.points is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var page = pdf.GetPage(annotation.pageIndex + 1);
|
||||
var canvas = new PdfCanvas(page);
|
||||
var color = ParseColor(annotation.strokeColor);
|
||||
canvas.SetStrokeColor(color);
|
||||
canvas.SetLineWidth(1);
|
||||
|
||||
foreach (var segment in annotation.lines.points)
|
||||
{
|
||||
var first = true;
|
||||
foreach (var point in segment)
|
||||
{
|
||||
var (px, py) = (ToInches(point[0]), ToInches(point[1]));
|
||||
if (first)
|
||||
{
|
||||
canvas.MoveTo(px, py);
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
canvas.LineTo(px, py);
|
||||
}
|
||||
}
|
||||
canvas.Stroke();
|
||||
}
|
||||
}
|
||||
|
||||
private static FontProvider CreateFontProvider()
|
||||
{
|
||||
var provider = new FontProvider();
|
||||
provider.AddStandardPdfFonts();
|
||||
provider.AddSystemFonts();
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void AddFormFieldValue(PdfDocument pdf, Annotation annotation, string value)
|
||||
{
|
||||
var bounds = annotation.bbox.Select(ToInches).ToList();
|
||||
var x = (float)bounds[0];
|
||||
var y = (float)bounds[1];
|
||||
var width = (float)bounds[2];
|
||||
var height = (float)bounds[3];
|
||||
|
||||
var page = pdf.GetPage(annotation.pageIndex + 1);
|
||||
var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
|
||||
canvas.SetProperty(Property.FONT_PROVIDER, FontProvider);
|
||||
canvas.SetProperty(Property.FONT, FontProvider.GetFontSet());
|
||||
|
||||
var paragraph = new Paragraph(value)
|
||||
.SetFontSize(_pdfBurnerParams.FontSize)
|
||||
.SetFontColor(ColorConstants.BLACK)
|
||||
.SetFontFamily(_pdfBurnerParams.FontName);
|
||||
|
||||
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Italic))
|
||||
{
|
||||
paragraph.SetItalic();
|
||||
}
|
||||
|
||||
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Bold))
|
||||
{
|
||||
paragraph.SetBold();
|
||||
}
|
||||
|
||||
canvas.ShowTextAligned(
|
||||
paragraph,
|
||||
x + (float)_pdfBurnerParams.TopMargin,
|
||||
y + (float)_pdfBurnerParams.YOffset,
|
||||
annotation.pageIndex + 1,
|
||||
iText.Layout.Properties.TextAlignment.LEFT,
|
||||
iText.Layout.Properties.VerticalAlignment.TOP,
|
||||
0);
|
||||
}
|
||||
|
||||
private static DeviceRgb ParseColor(string? color)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(color))
|
||||
{
|
||||
return new DeviceRgb(0, 0, 0);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var drawingColor = ColorTranslator.FromHtml(color);
|
||||
return new DeviceRgb(drawingColor.R, drawingColor.G, drawingColor.B);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new DeviceRgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static double ToInches(double value) => value / 72d;
|
||||
private static double ToInches(float value) => value / 72d;
|
||||
|
||||
#region Model
|
||||
private static class AnnotationType
|
||||
{
|
||||
public const string Image = "pspdfkit/image";
|
||||
public const string Ink = "pspdfkit/ink";
|
||||
public const string Widget = "pspdfkit/widget";
|
||||
}
|
||||
|
||||
private sealed class AnnotationData
|
||||
{
|
||||
public List<Annotation>? annotations { get; set; }
|
||||
public Dictionary<string, Attachment>? attachments { get; set; }
|
||||
public List<FormFieldValue>? formFieldValues { get; set; }
|
||||
}
|
||||
|
||||
private sealed class Annotation
|
||||
{
|
||||
public string id { get; set; } = string.Empty;
|
||||
public List<double> bbox { get; set; } = new();
|
||||
public string type { get; set; } = string.Empty;
|
||||
public string imageAttachmentId { get; set; } = string.Empty;
|
||||
public Lines? lines { get; set; }
|
||||
public int pageIndex { get; set; }
|
||||
public string strokeColor { get; set; } = string.Empty;
|
||||
public string egName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class Lines
|
||||
{
|
||||
public List<List<List<float>>> points { get; set; } = new();
|
||||
}
|
||||
|
||||
private sealed class Attachment
|
||||
{
|
||||
public string binary { get; set; } = string.Empty;
|
||||
public string contentType { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class FormFieldValue
|
||||
{
|
||||
public string name { get; set; } = string.Empty;
|
||||
public string value { get; set; } = string.Empty;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class PDFBurnerParams
|
||||
{
|
||||
public List<string> IgnoredLabels { get; } = new() { "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" };
|
||||
|
||||
public double TopMargin { get; set; } = 0.1;
|
||||
|
||||
public double YOffset { get; set; } = -0.3;
|
||||
|
||||
public string FontName { get; set; } = "Arial";
|
||||
|
||||
public int FontSize { get; set; } = 8;
|
||||
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Italic;
|
||||
}
|
||||
46
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFMerger.cs
Normal file
46
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFMerger.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.IO;
|
||||
using iText.Kernel.Pdf;
|
||||
using iText.Kernel.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class PDFMerger
|
||||
{
|
||||
private readonly ILogger<PDFMerger> _logger;
|
||||
|
||||
public PDFMerger() : this(NullLogger<PDFMerger>.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
public PDFMerger(ILogger<PDFMerger> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public byte[] MergeDocuments(byte[] document, byte[] report)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var finalStream = new MemoryStream();
|
||||
using var documentReader = new PdfReader(new MemoryStream(document));
|
||||
using var reportReader = new PdfReader(new MemoryStream(report));
|
||||
using var writer = new PdfWriter(finalStream);
|
||||
using var targetDoc = new PdfDocument(documentReader, writer);
|
||||
using var reportDoc = new PdfDocument(reportReader);
|
||||
|
||||
var merger = new PdfMerger(targetDoc);
|
||||
merger.Merge(reportDoc, 1, reportDoc.GetNumberOfPages());
|
||||
|
||||
targetDoc.Close();
|
||||
return finalStream.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to merge PDF documents");
|
||||
throw new MergeDocumentException("Documents could not be merged", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using iText.Kernel.Pdf;
|
||||
using iText.Layout.Element;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
using LayoutDocument = iText.Layout.Document;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class ReportCreator
|
||||
{
|
||||
private readonly ILogger<ReportCreator> _logger;
|
||||
|
||||
public ReportCreator() : this(NullLogger<ReportCreator>.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
public ReportCreator(ILogger<ReportCreator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public byte[] CreateReport(SqlConnection connection, Envelope envelope)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reportItems = LoadReportItems(connection, envelope.Id);
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new PdfWriter(stream);
|
||||
using var pdf = new PdfDocument(writer);
|
||||
using var document = new LayoutDocument(pdf);
|
||||
|
||||
document.Add(new Paragraph("Envelope Finalization Report").SetFontSize(16));
|
||||
document.Add(new Paragraph($"Envelope Id: {envelope.Id}"));
|
||||
document.Add(new Paragraph($"UUID: {envelope.Uuid}"));
|
||||
document.Add(new Paragraph($"Title: {envelope.Title}"));
|
||||
document.Add(new Paragraph($"Subject: {envelope.Comment}"));
|
||||
document.Add(new Paragraph($"Generated: {DateTime.UtcNow:O}"));
|
||||
document.Add(new Paragraph(" "));
|
||||
|
||||
var table = new Table(4).UseAllAvailableWidth();
|
||||
table.AddHeaderCell("Date");
|
||||
table.AddHeaderCell("Status");
|
||||
table.AddHeaderCell("User");
|
||||
table.AddHeaderCell("EnvelopeId");
|
||||
|
||||
foreach (var item in reportItems.OrderByDescending(r => r.ItemDate))
|
||||
{
|
||||
table.AddCell(item.ItemDate.ToString("u"));
|
||||
table.AddCell(item.ItemStatus.ToString());
|
||||
table.AddCell(item.ItemUserReference);
|
||||
table.AddCell(item.EnvelopeId.ToString());
|
||||
}
|
||||
|
||||
document.Add(table);
|
||||
document.Close();
|
||||
return stream.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not create report for envelope {EnvelopeId}", envelope.Id);
|
||||
throw new CreateReportException("Could not prepare report data", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ReportItem> LoadReportItems(SqlConnection connection, int envelopeId)
|
||||
{
|
||||
const string sql = "SELECT ENVELOPE_ID, POS_WHEN, POS_STATUS, POS_WHO FROM VWSIG_ENVELOPE_REPORT WHERE ENVELOPE_ID = @EnvelopeId";
|
||||
var result = new List<ReportItem>();
|
||||
|
||||
using var command = new SqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
result.Add(new ReportItem
|
||||
{
|
||||
EnvelopeId = reader.GetInt32(0),
|
||||
ItemDate = reader.IsDBNull(1) ? DateTime.MinValue : reader.GetDateTime(1),
|
||||
ItemStatus = reader.IsDBNull(2) ? default : (EnvelopeGenerator.Domain.Constants.EnvelopeStatus)reader.GetInt32(2),
|
||||
ItemUserReference = reader.IsDBNull(3) ? string.Empty : reader.GetString(3)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
19
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/ReportItem.cs
Normal file
19
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/ReportItem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class ReportItem
|
||||
{
|
||||
public Envelope? Envelope { get; set; }
|
||||
public int EnvelopeId { get; set; }
|
||||
public string EnvelopeTitle { get; set; } = string.Empty;
|
||||
public string EnvelopeSubject { get; set; } = string.Empty;
|
||||
|
||||
public EnvelopeStatus ItemStatus { get; set; }
|
||||
|
||||
public string ItemStatusTranslated => ItemStatus.ToString();
|
||||
|
||||
public string ItemUserReference { get; set; } = string.Empty;
|
||||
public DateTime ItemDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class ReportSource
|
||||
{
|
||||
public List<ReportItem> Items { get; set; } = new();
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
using iText.Kernel.Pdf.Canvas;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using iText.Kernel.Colors;
|
||||
using iText.Kernel.Geom;
|
||||
using DigitalData.Core.Abstractions.Interfaces;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
#endif
|
||||
@@ -19,6 +19,15 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
{
|
||||
return new Pdf<MemoryStream, MemoryStream>(new MemoryStream(documentBytes), new MemoryStream());
|
||||
}
|
||||
|
||||
public static Pdf<MemoryStream, MemoryStream> FromMemory(MemoryStream stream, MemoryStream
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
outputStream = null)
|
||||
{
|
||||
return new Pdf<MemoryStream, MemoryStream>(stream, outputStream ?? new MemoryStream(), disposeInputStream: false, disposeOutputStream: outputStream is null);
|
||||
}
|
||||
}
|
||||
|
||||
public class Pdf<TInputStream, TOutputStream> : IDisposable, IAsyncDisposable
|
||||
@@ -31,13 +40,18 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
private readonly PdfReader _reader;
|
||||
private readonly PdfWriter _writer;
|
||||
|
||||
public Pdf(TInputStream inputStream, TOutputStream outputStream)
|
||||
private readonly bool _disposeInputStream;
|
||||
private readonly bool _disposeOutputStream;
|
||||
|
||||
public Pdf(TInputStream inputStream, TOutputStream outputStream, bool disposeInputStream = true, bool disposeOutputStream = true)
|
||||
{
|
||||
_inputStream = inputStream;
|
||||
_outputStream = outputStream;
|
||||
_reader = new PdfReader(inputStream);
|
||||
_writer = new PdfWriter(outputStream);
|
||||
_doc = new PdfDocument(_reader, _writer);
|
||||
_disposeInputStream = disposeInputStream;
|
||||
_disposeOutputStream = disposeOutputStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -88,7 +102,7 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
});
|
||||
#endregion
|
||||
|
||||
public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures)
|
||||
public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthPx = 1.9500000000000002, double heightPx = 2.52)
|
||||
where TSignature : ISignature
|
||||
{
|
||||
// once per page
|
||||
@@ -106,8 +120,8 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
double magin = .2;
|
||||
double x = (signature.X - .7 - magin) * inchFactor;
|
||||
double y = (signature.Y - .5 - magin) * inchFactor;
|
||||
double width = 1.9500000000000002 * inchFactor;
|
||||
double height = 2.52 * inchFactor;
|
||||
double width = widthPx * inchFactor;
|
||||
double height = heightPx * inchFactor;
|
||||
|
||||
double bottomLineLength = 2.5;
|
||||
|
||||
@@ -149,16 +163,16 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
{
|
||||
// Managed resources
|
||||
_doc?.Close();
|
||||
_inputStream?.Dispose();
|
||||
_outputStream?.Dispose();
|
||||
if (_disposeInputStream) _inputStream?.Dispose();
|
||||
if(_disposeOutputStream) _outputStream?.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// When called by the finalizer, clean up only unmanaged resources
|
||||
// Unmanaged resources such as PdfDocument, PdfReader, and PdfWriter are already IDisposable; we close them here
|
||||
try { _doc?.Close(); } catch { }
|
||||
try { _inputStream?.Dispose(); } catch { }
|
||||
try { _outputStream?.Dispose(); } catch { }
|
||||
try { if(_disposeInputStream) _inputStream?.Dispose(); } catch { }
|
||||
try { if (_disposeOutputStream) _outputStream?.Dispose(); } catch { }
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
@@ -170,15 +184,21 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
return;
|
||||
|
||||
_doc?.Close();
|
||||
if (_inputStream is IAsyncDisposable asyncInput)
|
||||
await asyncInput.DisposeAsync();
|
||||
else
|
||||
_inputStream.Dispose();
|
||||
if (_disposeInputStream)
|
||||
{
|
||||
if (_inputStream is IAsyncDisposable asyncInput)
|
||||
await asyncInput.DisposeAsync();
|
||||
else
|
||||
_inputStream.Dispose();
|
||||
}
|
||||
|
||||
if (_outputStream is IAsyncDisposable asyncOutput)
|
||||
await asyncOutput.DisposeAsync();
|
||||
else
|
||||
_outputStream?.Dispose();
|
||||
if (_disposeOutputStream)
|
||||
{
|
||||
if (_outputStream is IAsyncDisposable asyncOutput)
|
||||
await asyncOutput.DisposeAsync();
|
||||
else
|
||||
_outputStream?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.1.1.0" newVersion="2.1.1.0" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Text.Encodings.Web" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
|
||||
@@ -17,6 +17,21 @@
|
||||
<TargetFrameworkProfile />
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
<Install>true</Install>
|
||||
<InstallFrom>Disk</InstallFrom>
|
||||
<UpdateEnabled>false</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateInterval>7</UpdateInterval>
|
||||
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
||||
<UpdatePeriodically>false</UpdatePeriodically>
|
||||
<UpdateRequired>false</UpdateRequired>
|
||||
<MapFileExtensions>true</MapFileExtensions>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||
<UseApplicationTrust>false</UseApplicationTrust>
|
||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -54,6 +69,9 @@
|
||||
<Reference Include="BouncyCastle.Cryptography, Version=2.0.0.0, Culture=neutral, PublicKeyToken=072edcf4a5328938, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\BouncyCastle.Cryptography.2.5.0\lib\net461\BouncyCastle.Cryptography.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Modules.Base, Version=1.3.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\2_DLL Projekte\DDModules\Base\bin\Debug\DigitalData.Modules.Base.dll</HintPath>
|
||||
@@ -160,8 +178,14 @@
|
||||
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.Logging.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.DependencyInjection.7.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.Logging.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
@@ -335,6 +359,18 @@
|
||||
<Name>EnvelopeGenerator.Domain</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include=".NETFramework,Version=v4.6.2">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>Microsoft .NET Framework 4.6.2 %28x86 and x64%29</ProductName>
|
||||
<Install>true</Install>
|
||||
</BootstrapperPackage>
|
||||
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
||||
<Install>false</Install>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
|
||||
<Import Project="..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets" Condition="Exists('..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
|
||||
16
EnvelopeGenerator.Service/UserConfig.xml
Normal file
16
EnvelopeGenerator.Service/UserConfig.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0"?>
|
||||
<Config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<ConnectionString>Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;</ConnectionString>
|
||||
<Debug>true</Debug>
|
||||
<IntervalInMin>1</IntervalInMin>
|
||||
<IgnoredLabels>
|
||||
<Label>Date</Label>
|
||||
<Label>Datum</Label>
|
||||
<Label>ZIP</Label>
|
||||
<Label>PLZ</Label>
|
||||
<Label>Place</Label>
|
||||
<Label>Ort</Label>
|
||||
<Label>Position</Label>
|
||||
<Label>Stellung</Label>
|
||||
</IgnoredLabels>
|
||||
</Config>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net48" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
|
||||
<package id="DocumentFormat.OpenXml" version="3.2.0" targetFramework="net48" />
|
||||
<package id="DocumentFormat.OpenXml.Framework" version="3.2.0" targetFramework="net48" />
|
||||
<package id="EntityFramework" version="6.4.4" targetFramework="net48" />
|
||||
@@ -11,7 +12,9 @@
|
||||
<package id="Microsoft.AspNet.WebApi.Client" version="6.0.0" targetFramework="net48" />
|
||||
<package id="Microsoft.Bcl.AsyncInterfaces" version="8.0.0" targetFramework="net48" />
|
||||
<package id="Microsoft.CSharp" version="4.7.0" targetFramework="net48" />
|
||||
<package id="Microsoft.Extensions.Logging.Abstractions" version="2.1.1" targetFramework="net462" />
|
||||
<package id="Microsoft.Extensions.DependencyInjection" version="7.0.0" targetFramework="net462" />
|
||||
<package id="Microsoft.Extensions.DependencyInjection.Abstractions" version="7.0.0" targetFramework="net462" />
|
||||
<package id="Microsoft.Extensions.Logging.Abstractions" version="7.0.0" targetFramework="net462" />
|
||||
<package id="Microsoft.VisualBasic" version="10.3.0" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json.Bson" version="1.0.2" targetFramework="net48" />
|
||||
|
||||
@@ -5,6 +5,7 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace EnvelopeGenerator.Tests.Application;
|
||||
@@ -44,9 +45,15 @@ public class DocSignedNotificationTests : TestBase
|
||||
|
||||
// Create envelope receiver
|
||||
var envRcv = this.CreateEnvelopeReceiver(env!.Id, rcv.Id);
|
||||
envRcv = await Repository.CreateAsync(envRcv, cancel);
|
||||
|
||||
var repo = GetRepository<EnvelopeReceiver>();
|
||||
|
||||
envRcv = await repo.CreateAsync(envRcv, cancel);
|
||||
var envRcvDto = _mapper.Map<EnvelopeReceiverDto>(envRcv);
|
||||
var docSignedNtf = envRcvDto.ToDocSignedNotification(new () { });
|
||||
|
||||
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
|
||||
|
||||
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
|
||||
|
||||
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Bogus;
|
||||
using CommandDotNet;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.UserManager.Domain.Entities;
|
||||
using EnvelopeGenerator.Application;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using EnvelopeGenerator.Application.Histories.Commands;
|
||||
@@ -11,6 +13,7 @@ using EnvelopeGenerator.Application.Users.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Infrastructure;
|
||||
using EnvelopeGenerator.Tests.Application;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -20,7 +23,6 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
|
||||
namespace EnvelopeGenerator.Tests.Application;
|
||||
|
||||
@@ -42,10 +44,29 @@ public class Fake
|
||||
// add Application and Infrastructure services
|
||||
#pragma warning disable CS0618
|
||||
services.AddEnvelopeGeneratorServices(configuration);
|
||||
services.AddEnvelopeGeneratorInfrastructureServices(
|
||||
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
|
||||
context.Configuration
|
||||
);
|
||||
|
||||
var cnnStrName = "Default";
|
||||
|
||||
var connStr = context.Configuration.GetConnectionString(cnnStrName)
|
||||
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
|
||||
|
||||
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
||||
{
|
||||
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
|
||||
|
||||
opt.AddDbTriggerParams(context.Configuration);
|
||||
|
||||
opt.AddDbContext((provider, options) =>
|
||||
{
|
||||
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
|
||||
options.UseSqlServer(connStr)
|
||||
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableDetailedErrors();
|
||||
});
|
||||
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
|
||||
|
||||
});
|
||||
|
||||
var prodCnnStr = context.Configuration.GetConnectionString("Default");
|
||||
services.AddDbContext<EGDbContext2Prod>(opt => opt.UseSqlServer(prodCnnStr));
|
||||
@@ -24,6 +24,8 @@ public abstract class TestBase : Faker
|
||||
|
||||
protected IRepository Repository => Host.Services.GetRequiredService<IRepository>();
|
||||
|
||||
protected IServiceProvider Services => Host.Services;
|
||||
|
||||
protected abstract void ConfigureServices(IServiceCollection services);
|
||||
|
||||
[SetUp]
|
||||
@@ -32,9 +34,11 @@ public abstract class TestBase : Faker
|
||||
Host = Fake.CreateHost(ConfigureServices);
|
||||
await Host.AddSamples();
|
||||
|
||||
var repo = GetRepository<EmailTemplate>();
|
||||
|
||||
// Add seed email templates
|
||||
foreach (var temp in SeedEmailTemplates)
|
||||
await Repository.CreateAsync(temp);
|
||||
await repo.CreateAsync(temp);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
17
EnvelopeGenerator.Tests/Domain/ConstantsTests.cs
Normal file
17
EnvelopeGenerator.Tests/Domain/ConstantsTests.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace EnvelopeGenerator.Tests.Domain;
|
||||
|
||||
public class ConstantsTests
|
||||
{
|
||||
[TestCase(EnvelopeSigningType.ReadAndSign, EnvelopeSigningType.ReadAndSign)]
|
||||
[TestCase(EnvelopeSigningType.WetSignature, EnvelopeSigningType.WetSignature)]
|
||||
[TestCase((EnvelopeSigningType)5, EnvelopeSigningType.WetSignature)]
|
||||
public void Normalize_ReturnsExpectedValue(EnvelopeSigningType input, EnvelopeSigningType expected)
|
||||
{
|
||||
var normalized = input.Normalize();
|
||||
|
||||
Assert.That(normalized, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.3.5" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.1.1" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
@@ -4,6 +4,7 @@ using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Web.Extensions;
|
||||
using MediatR;
|
||||
@@ -11,7 +12,6 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.Web.Controllers;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class AnnotationController : ControllerBase
|
||||
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] ExpandoObject annotations, CancellationToken cancel = default)
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||
{
|
||||
// get claims
|
||||
var signature = User.GetAuthReceiverSignature();
|
||||
@@ -56,22 +56,30 @@ public class AnnotationController : ControllerBase
|
||||
return Unauthorized("User authentication is incomplete. Missing required claims for processing this request.");
|
||||
}
|
||||
|
||||
// check if non read-and-confirm envelope is signed without annotation
|
||||
var er = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
||||
|
||||
if (!er.Envelope!.ReadOnly && psPdfKitAnnotation is null)
|
||||
return BadRequest();
|
||||
|
||||
// Again check if receiver has already signed
|
||||
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
||||
return Problem(statusCode: 403);
|
||||
return Problem(statusCode: 409);
|
||||
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||
return Problem(statusCode: 423);
|
||||
|
||||
var docSignedNotification = await _mediator
|
||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
.ToDocSignedNotification(annotations)
|
||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
|
||||
await _mediator.Publish(docSignedNotification, cancel);
|
||||
await _mediator.PublishSafely(docSignedNotification, cancel);
|
||||
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[HttpPost("reject")]
|
||||
[Obsolete("Use DigitalData.Core.Exceptions and .Middleware")]
|
||||
|
||||
@@ -95,6 +95,23 @@ public class EnvelopeController : ViewControllerBase
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UseAccessCode
|
||||
if (!er.Envelope!.UseAccessCode)
|
||||
{
|
||||
(string? uuid, string? signature) = decoded.ParseEnvelopeReceiverId();
|
||||
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid!, signature: signature!);
|
||||
|
||||
if (er_secret_res.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(er_secret_res.Notices);
|
||||
return this.ViewEnvelopeNotFound();
|
||||
}
|
||||
var er_secret = er_secret_res.Data;
|
||||
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
|
||||
return await CreateShowEnvelopeView(er_secret);
|
||||
}
|
||||
#endregion UseAccessCode
|
||||
|
||||
#region Send Access Code
|
||||
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress);
|
||||
if (!accessCodeAlreadyRequested)
|
||||
@@ -121,7 +138,7 @@ public class EnvelopeController : ViewControllerBase
|
||||
|
||||
[HttpPost("{envelopeReceiverId}")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth)
|
||||
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth, CancellationToken cancel)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -145,6 +162,15 @@ public class EnvelopeController : ViewControllerBase
|
||||
}
|
||||
var er_secret = er_secret_res.Data;
|
||||
|
||||
//check rejection
|
||||
var rejRcvrs = await _historyService.ReadRejectingReceivers(er_secret.Envelope!.Id);
|
||||
if (rejRcvrs.Any())
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
ViewBag.IsExt = !rejRcvrs.Contains(er_secret.Receiver); //external if the current user is not rejected
|
||||
return View("EnvelopeRejected", er_secret);
|
||||
}
|
||||
|
||||
// show envelope if already logged in
|
||||
if (User.IsInRole(ReceiverRole.FullyAuth))
|
||||
return await CreateShowEnvelopeView(er_secret);
|
||||
@@ -190,7 +216,7 @@ public class EnvelopeController : ViewControllerBase
|
||||
return this.ViewInnerServiceError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<IActionResult> CreateEnvelopeLockedView(EnvelopeReceiverDto er, CancellationToken cancel)
|
||||
{
|
||||
var uuidClaim = User.GetAuthEnvelopeUuid();
|
||||
@@ -222,7 +248,9 @@ public class EnvelopeController : ViewControllerBase
|
||||
{
|
||||
if (er.Envelope!.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
|
||||
{
|
||||
using var pdf = Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
|
||||
using var pdf = er.Envelope.ReadOnly
|
||||
? Pdf.FromMemory(doc.ByteData)
|
||||
: Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
|
||||
|
||||
doc.ByteData = pdf.ExportAsBytes();
|
||||
|
||||
@@ -236,6 +264,8 @@ public class EnvelopeController : ViewControllerBase
|
||||
|
||||
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
|
||||
|
||||
ViewData["ReadAndConfirm"] = er.Envelope.ReadOnly;
|
||||
|
||||
//add PSPDFKit licence key
|
||||
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Web.Controllers.Test;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class TestAnnotationController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public TestAnnotationController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
[HttpDelete("{envelopeKey}")]
|
||||
public async Task<IActionResult> Delete([FromRoute] string envelopeKey)
|
||||
{
|
||||
if (envelopeKey.GetEnvelopeUuid() is not string uuid)
|
||||
return BadRequest();
|
||||
|
||||
if (envelopeKey.GetReceiverSignature() is not string signature)
|
||||
return BadRequest();
|
||||
|
||||
await _mediator.Publish(new RemoveSignatureNotification()
|
||||
{
|
||||
EnvelopeUuid = uuid,
|
||||
ReceiverSignature = signature
|
||||
});
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
49
EnvelopeGenerator.Web/EnvelopeCookieManager.cs
Normal file
49
EnvelopeGenerator.Web/EnvelopeCookieManager.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
||||
namespace EnvelopeGenerator.Web;
|
||||
|
||||
public class EnvelopeCookieManager : ICookieManager
|
||||
{
|
||||
private readonly IEnumerable<string> _envelopeKeyBasedCookieNames;
|
||||
|
||||
private readonly ChunkingCookieManager _inner = new();
|
||||
|
||||
public EnvelopeCookieManager(params string[] envelopeKeyBasedCookieNames)
|
||||
{
|
||||
_envelopeKeyBasedCookieNames = envelopeKeyBasedCookieNames;
|
||||
}
|
||||
|
||||
private string GetCookieName(HttpContext context, string key)
|
||||
{
|
||||
if (!_envelopeKeyBasedCookieNames.Contains(key))
|
||||
return key;
|
||||
|
||||
var envId = context.GetRouteValue("envelopeReceiverId")?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(envId) && context.Request.Query.TryGetValue("envKey", out var envKeyValue))
|
||||
envId = envKeyValue;
|
||||
|
||||
if (string.IsNullOrEmpty(envId))
|
||||
return key;
|
||||
|
||||
return $"{key}-{envId}";
|
||||
}
|
||||
|
||||
public string? GetRequestCookie(HttpContext context, string key)
|
||||
{
|
||||
var cookieName = GetCookieName(context, key);
|
||||
return _inner.GetRequestCookie(context, cookieName);
|
||||
}
|
||||
|
||||
public void AppendResponseCookie(HttpContext context, string key, string? value, CookieOptions options)
|
||||
{
|
||||
var cookieName = GetCookieName(context, key);
|
||||
_inner.AppendResponseCookie(context, cookieName, value, options);
|
||||
}
|
||||
|
||||
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
|
||||
{
|
||||
var cookieName = GetCookieName(context, key);
|
||||
_inner.DeleteCookie(context, cookieName, options);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PackageId>EnvelopeGenerator.Web</PackageId>
|
||||
@@ -12,9 +12,9 @@
|
||||
<PackageTags>digital data envelope generator web</PackageTags>
|
||||
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
||||
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
||||
<Version>3.4.1</Version>
|
||||
<AssemblyVersion>3.4.1</AssemblyVersion>
|
||||
<FileVersion>3.4.1</FileVersion>
|
||||
<Version>3.9.0</Version>
|
||||
<AssemblyVersion>3.9.0</AssemblyVersion>
|
||||
<FileVersion>3.9.0</FileVersion>
|
||||
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2094,20 +2094,13 @@
|
||||
<None Include="wwwroot\lib\bootstrap-icons\icons\zoom-out.svg" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
@@ -2126,6 +2119,56 @@
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
<PackageReference Include="Quartz" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
<PackageReference Include="Quartz" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.11" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.5" />
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||
|
||||
@@ -17,6 +17,7 @@ using EnvelopeGenerator.Web.Models.Annotation;
|
||||
using DigitalData.UserManager.DependencyInjection;
|
||||
using EnvelopeGenerator.Web.Middleware;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Web;
|
||||
|
||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||
logger.Info("Logging initialized!");
|
||||
@@ -134,41 +135,22 @@ try
|
||||
options.ConsentCookie.Name = "cookie-consent-settings";
|
||||
});
|
||||
|
||||
var authCookieName = "env_auth";
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
|
||||
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
|
||||
options.Cookie.Name = authCookieName;
|
||||
options.CookieManager = new EnvelopeCookieManager(authCookieName);
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
|
||||
|
||||
options.Events = new CookieAuthenticationEvents
|
||||
{
|
||||
OnRedirectToLogin = context =>
|
||||
{
|
||||
// Dynamically calculate the redirection path, for example:
|
||||
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
|
||||
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
|
||||
|
||||
context.Response.Redirect(context.RedirectUri);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnRedirectToLogout = context =>
|
||||
{
|
||||
// Apply a similar redirection logic for logout
|
||||
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
|
||||
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
|
||||
|
||||
context.Response.Redirect(context.RedirectUri);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton(config.GetSection("ContactLink").Get<ContactLink>() ?? new());
|
||||
|
||||
builder.Services.AddCookieBasedLocalizer();
|
||||
|
||||
|
||||
builder.Services.AddSingleton(HtmlEncoder.Default);
|
||||
builder.Services.AddSingleton(UrlEncoder.Default);
|
||||
builder.Services.AddSanitizer<HtmlSanitizer>();
|
||||
@@ -249,7 +231,7 @@ try
|
||||
app.UseAuthorization();
|
||||
|
||||
var cultures = app.Services.GetRequiredService<Cultures>();
|
||||
if(!cultures.Any())
|
||||
if (!cultures.Any())
|
||||
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
|
||||
Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
|
||||
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
|
||||
@@ -264,7 +246,7 @@ try
|
||||
}
|
||||
]");
|
||||
|
||||
if(!config.GetValue<bool>("DisableMultiLanguage"))
|
||||
if (!config.GetValue<bool>("DisableMultiLanguage"))
|
||||
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
|
||||
|
||||
app.UseCors("SameOriginPolicy");
|
||||
@@ -273,7 +255,7 @@ try
|
||||
app.MapFallbackToController("Error404", "Home");
|
||||
app.Run();
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Stopped program because of exception");
|
||||
throw;
|
||||
|
||||
@@ -11,12 +11,10 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Web\net9\win64\$(Version)\EnvelopeGenerator.Web.zip</DesktopBuildPackageLocation>
|
||||
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Web\net8\$(Version)\EnvelopeGenerator.Web.zip</DesktopBuildPackageLocation>
|
||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -43,12 +43,14 @@
|
||||
</svg>
|
||||
<span>@_localizer.Reject()</span>
|
||||
</button>
|
||||
@if(!Model.Envelope!.ReadOnly){
|
||||
<button class="btn_refresh btn btn-secondary btn-desktop" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
|
||||
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="dd-cards-container">
|
||||
@@ -79,7 +81,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<h6>@($"{@envelope?.Message}")</h6>
|
||||
<div class="markdown">@(envelope?.Message)</div>
|
||||
}
|
||||
<p>
|
||||
<small class="text-body-secondary">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user