Compare commits
65 Commits
7aeaba7c12
...
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 |
@@ -74,6 +74,11 @@ public record EnvelopeDto
|
||||
/// </summary>
|
||||
public int? EnvelopeTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool ReadOnly => EnvelopeTypeId == 2;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
|
||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -59,7 +59,7 @@ public static class DocSignedNotificationExtensions
|
||||
/// <param name="dtoTask"></param>
|
||||
/// <param name="psPdfKitAnnotation"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation psPdfKitAnnotation)
|
||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
|
||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -29,6 +29,9 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
=> _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
|
||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
{
|
||||
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
|
||||
await _repo.CreateAsync(annot.Structured, cancel);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// </summary>
|
||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private const string BlankAnnotationJson = "{}";
|
||||
|
||||
private readonly ISender _sender;
|
||||
|
||||
/// <summary>
|
||||
@@ -33,7 +35,9 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
Envelope = new() { Id = notification.EnvelopeId },
|
||||
Receiver = new() { Id = notification.ReceiverId},
|
||||
Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
|
||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||
: BlankAnnotationJson
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
|
||||
<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;
|
||||
}
|
||||
}
|
||||
@@ -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,8 +70,8 @@
|
||||
<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.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\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.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.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">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<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.4.0" 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" />
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
<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.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\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.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
|
||||
@@ -422,7 +422,7 @@ Namespace Jobs
|
||||
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)
|
||||
|
||||
@@ -37,6 +37,15 @@ Namespace Jobs.FinalizeDocument
|
||||
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) _
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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.4.0" 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" />
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.5" />
|
||||
<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();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
@@ -1,96 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
@page "/counter"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
@@ -1,64 +0,0 @@
|
||||
@page "/weather"
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Temp. (C)</th>
|
||||
<th>Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Simulate asynchronous loading to demonstrate streaming rendering
|
||||
await Task.Delay(500);
|
||||
|
||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = startDate.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int TemperatureC { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -1,10 +0,0 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.ReceiverUI
|
||||
@using EnvelopeGenerator.ReceiverUI.Components
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,27 +0,0 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Components;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:26087",
|
||||
"sslPort": 44331
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5134",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7124;http://localhost:5134",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -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,7 +20,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
|
||||
<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" />
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"homepage": "https://slavik0329.github.io/pdf-sign",
|
||||
"name": "pdf-sign",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"annotpdf": "^1.0.12",
|
||||
"dayjs": "^1.9.6",
|
||||
"pdf-lib": "^1.12.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-draggable": "^4.4.3",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-icons": "^4.1.0",
|
||||
"react-pdf": "^5.0.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"react-signature-canvas": "^1.0.3",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"predeploy": "npm run build",
|
||||
"deploy": "gh-pages -d build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"gh-pages": "^3.1.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Finally, a simple way to put your signature on a PDF for free."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>PDF-sign</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
body {
|
||||
font-family: "Open Sans";
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus,
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
import "./App.css";
|
||||
import { useRef, useState } from "react";
|
||||
import Drop from "./Drop";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
import { PDFDocument, rgb } from "pdf-lib";
|
||||
import { blobToURL } from "./utils/Utils";
|
||||
import PagingControl from "./components/PagingControl";
|
||||
import { AddSigDialog } from "./components/AddSigDialog";
|
||||
import { Header } from "./Header";
|
||||
import { BigButton } from "./components/BigButton";
|
||||
import DraggableSignature from "./components/DraggableSignature";
|
||||
import DraggableText from "./components/DraggableText";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
|
||||
|
||||
function downloadURI(uri, name) {
|
||||
var link = document.createElement("a");
|
||||
link.download = name;
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const styles = {
|
||||
container: {
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
},
|
||||
sigBlock: {
|
||||
display: "inline-block",
|
||||
border: "1px solid #000",
|
||||
},
|
||||
documentBlock: {
|
||||
maxWidth: 800,
|
||||
margin: "20px auto",
|
||||
marginTop: 8,
|
||||
border: "1px solid #999",
|
||||
},
|
||||
controls: {
|
||||
maxWidth: 800,
|
||||
margin: "0 auto",
|
||||
marginTop: 8,
|
||||
},
|
||||
};
|
||||
const [pdf, setPdf] = useState(null);
|
||||
const [autoDate, setAutoDate] = useState(true);
|
||||
const [signatureURL, setSignatureURL] = useState(null);
|
||||
const [position, setPosition] = useState(null);
|
||||
const [signatureDialogVisible, setSignatureDialogVisible] = useState(false);
|
||||
const [textInputVisible, setTextInputVisible] = useState(false);
|
||||
const [pageNum, setPageNum] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [pageDetails, setPageDetails] = useState(null);
|
||||
const documentRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<div style={styles.container}>
|
||||
{signatureDialogVisible ? (
|
||||
<AddSigDialog
|
||||
autoDate={autoDate}
|
||||
setAutoDate={setAutoDate}
|
||||
onClose={() => setSignatureDialogVisible(false)}
|
||||
onConfirm={(url) => {
|
||||
setSignatureURL(url);
|
||||
setSignatureDialogVisible(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!pdf ? (
|
||||
<Drop
|
||||
onLoaded={async (files) => {
|
||||
const URL = await blobToURL(files[0]);
|
||||
setPdf(URL);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{pdf ? (
|
||||
<div>
|
||||
<div style={styles.controls}>
|
||||
{!signatureURL ? (
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Add signature"}
|
||||
onClick={() => setSignatureDialogVisible(true)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Add Date"}
|
||||
onClick={() => setTextInputVisible("date")}
|
||||
/>
|
||||
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Add Text"}
|
||||
onClick={() => setTextInputVisible(true)}
|
||||
/>
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Reset"}
|
||||
onClick={() => {
|
||||
setTextInputVisible(false);
|
||||
setSignatureDialogVisible(false);
|
||||
setSignatureURL(null);
|
||||
setPdf(null);
|
||||
setTotalPages(0);
|
||||
setPageNum(0);
|
||||
setPageDetails(null);
|
||||
}}
|
||||
/>
|
||||
{pdf ? (
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
inverted={true}
|
||||
title={"Download"}
|
||||
onClick={() => {
|
||||
downloadURI(pdf, "file.pdf");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div ref={documentRef} style={styles.documentBlock}>
|
||||
{textInputVisible ? (
|
||||
<DraggableText
|
||||
initialText={
|
||||
textInputVisible === "date"
|
||||
? dayjs().format("M/d/YYYY")
|
||||
: null
|
||||
}
|
||||
onCancel={() => setTextInputVisible(false)}
|
||||
onEnd={setPosition}
|
||||
onSet={async (text) => {
|
||||
const { originalHeight, originalWidth } = pageDetails;
|
||||
const scale = originalWidth / documentRef.current.clientWidth;
|
||||
|
||||
const y =
|
||||
documentRef.current.clientHeight -
|
||||
(position.y +
|
||||
(12 * scale) -
|
||||
position.offsetY -
|
||||
documentRef.current.offsetTop);
|
||||
const x =
|
||||
position.x -
|
||||
166 -
|
||||
position.offsetX -
|
||||
documentRef.current.offsetLeft;
|
||||
|
||||
// new XY in relation to actual document size
|
||||
const newY =
|
||||
(y * originalHeight) / documentRef.current.clientHeight;
|
||||
const newX =
|
||||
(x * originalWidth) / documentRef.current.clientWidth;
|
||||
|
||||
const pdfDoc = await PDFDocument.load(pdf);
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
const firstPage = pages[pageNum];
|
||||
|
||||
firstPage.drawText(text, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
size: 20 * scale,
|
||||
});
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const blob = new Blob([new Uint8Array(pdfBytes)]);
|
||||
|
||||
const URL = await blobToURL(blob);
|
||||
setPdf(URL);
|
||||
setPosition(null);
|
||||
setTextInputVisible(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{signatureURL ? (
|
||||
<DraggableSignature
|
||||
url={signatureURL}
|
||||
onCancel={() => {
|
||||
setSignatureURL(null);
|
||||
}}
|
||||
onSet={async () => {
|
||||
const { originalHeight, originalWidth } = pageDetails;
|
||||
const scale = originalWidth / documentRef.current.clientWidth;
|
||||
|
||||
const y =
|
||||
documentRef.current.clientHeight -
|
||||
(position.y -
|
||||
position.offsetY +
|
||||
64 -
|
||||
documentRef.current.offsetTop);
|
||||
const x =
|
||||
position.x -
|
||||
160 -
|
||||
position.offsetX -
|
||||
documentRef.current.offsetLeft;
|
||||
|
||||
// new XY in relation to actual document size
|
||||
const newY =
|
||||
(y * originalHeight) / documentRef.current.clientHeight;
|
||||
const newX =
|
||||
(x * originalWidth) / documentRef.current.clientWidth;
|
||||
|
||||
const pdfDoc = await PDFDocument.load(pdf);
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
const firstPage = pages[pageNum];
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(signatureURL);
|
||||
const pngDims = pngImage.scale( scale * .3);
|
||||
|
||||
firstPage.drawImage(pngImage, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
width: pngDims.width,
|
||||
height: pngDims.height,
|
||||
});
|
||||
|
||||
if (autoDate) {
|
||||
firstPage.drawText(
|
||||
`Signed ${dayjs().format(
|
||||
"M/d/YYYY HH:mm:ss ZZ"
|
||||
)}`,
|
||||
{
|
||||
x: newX,
|
||||
y: newY - 10,
|
||||
size: 14 * scale,
|
||||
color: rgb(0.074, 0.545, 0.262),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const blob = new Blob([new Uint8Array(pdfBytes)]);
|
||||
|
||||
const URL = await blobToURL(blob);
|
||||
setPdf(URL);
|
||||
setPosition(null);
|
||||
setSignatureURL(null);
|
||||
}}
|
||||
onEnd={setPosition}
|
||||
/>
|
||||
) : null}
|
||||
<Document
|
||||
file={pdf}
|
||||
onLoadSuccess={(data) => {
|
||||
setTotalPages(data.numPages);
|
||||
}}
|
||||
>
|
||||
<Page
|
||||
pageNumber={pageNum + 1}
|
||||
width={800}
|
||||
height={1200}
|
||||
onLoadSuccess={(data) => {
|
||||
setPageDetails(data);
|
||||
}}
|
||||
/>
|
||||
</Document>
|
||||
</div>
|
||||
<PagingControl
|
||||
pageNum={pageNum}
|
||||
setPageNum={setPageNum}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { cleanBorder, primary45 } from "./utils/colors";
|
||||
|
||||
export default function Drop({ onLoaded }) {
|
||||
const styles = {
|
||||
container: {
|
||||
textAlign: "center",
|
||||
border: cleanBorder,
|
||||
padding: 20,
|
||||
marginTop: 12,
|
||||
color: primary45,
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
borderRadius: 4,
|
||||
userSelect: "none",
|
||||
outline: 0,
|
||||
cursor: "pointer",
|
||||
},
|
||||
};
|
||||
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
onLoaded(acceptedFiles);
|
||||
// Do something with the files
|
||||
}, []);
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: "application/pdf",
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...getRootProps()} style={styles.container}>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? <p>Drop a PDF here</p> : <p>Drag a PDF here</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import {primary45} from "./utils/colors";
|
||||
|
||||
export function Header() {
|
||||
const styles = {
|
||||
container: {
|
||||
backgroundColor: primary45,
|
||||
color: '#FFF',
|
||||
padding: 12,
|
||||
fontWeight: 600,
|
||||
}
|
||||
}
|
||||
return <div style={styles.container}>
|
||||
<div>Open PDF Sign</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Dialog } from "./Dialog";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { ConfirmOrCancel } from "./ConfirmOrCancel";
|
||||
import { primary45 } from "../utils/colors";
|
||||
import { useRef } from "react";
|
||||
|
||||
export function AddSigDialog({ onConfirm, onClose, autoDate, setAutoDate }) {
|
||||
const sigRef = useRef(null);
|
||||
|
||||
const styles = {
|
||||
sigContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
sigBlock: {
|
||||
display: "inline-block",
|
||||
border: `1px solid ${primary45}`,
|
||||
},
|
||||
instructions: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
textAlign: "center",
|
||||
color: primary45,
|
||||
marginTop: 8,
|
||||
width: 600,
|
||||
alignSelf: "center",
|
||||
},
|
||||
instructionsContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
isVisible={true}
|
||||
title={"Add signature"}
|
||||
body={
|
||||
<div style={styles.container}>
|
||||
<div style={styles.sigContainer}>
|
||||
<div style={styles.sigBlock}>
|
||||
<SignatureCanvas
|
||||
velocityFilterWeight={1}
|
||||
ref={sigRef}
|
||||
canvasProps={{
|
||||
width: "600",
|
||||
height: 200,
|
||||
className: "sigCanvas",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.instructionsContainer}>
|
||||
<div style={styles.instructions}>
|
||||
<div>
|
||||
Auto date/time{" "}
|
||||
<input
|
||||
type={"checkbox"}
|
||||
checked={autoDate}
|
||||
onChange={(e) => setAutoDate(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div>Draw your signature above</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmOrCancel
|
||||
onCancel={onClose}
|
||||
onConfirm={() => {
|
||||
const sigURL = sigRef.current.toDataURL();
|
||||
onConfirm(sigURL);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from "react";
|
||||
import { primary45 } from "../utils/colors";
|
||||
import useHover from "../hooks/useHover";
|
||||
|
||||
export function BigButton({
|
||||
title,
|
||||
onClick,
|
||||
inverted,
|
||||
fullWidth,
|
||||
customFillColor,
|
||||
customWhiteColor,
|
||||
style,
|
||||
noHover,
|
||||
id,
|
||||
small,
|
||||
disabled,
|
||||
marginRight,
|
||||
}) {
|
||||
const [hoverRef, isHovered] = useHover();
|
||||
|
||||
let fillColor = customFillColor || primary45;
|
||||
const whiteColor = customWhiteColor || "#FFF";
|
||||
|
||||
let initialBg = null;
|
||||
let hoverBg = fillColor;
|
||||
|
||||
let initialColor = fillColor;
|
||||
let hoverColor = whiteColor;
|
||||
|
||||
if (inverted) {
|
||||
initialBg = fillColor;
|
||||
hoverBg = null;
|
||||
initialColor = whiteColor;
|
||||
hoverColor = fillColor;
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
initialBg = "#ddd";
|
||||
hoverBg = "#ddd";
|
||||
fillColor = "#ddd";
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: fullWidth ? "100%" : null,
|
||||
backgroundColor: isHovered && !noHover ? hoverBg : initialBg,
|
||||
color:
|
||||
isHovered && !noHover && !disabled
|
||||
? hoverColor
|
||||
: disabled
|
||||
? "#999"
|
||||
: initialColor,
|
||||
borderRadius: 4,
|
||||
padding: small ? "2px 4px" : "6px 8px",
|
||||
fontSize: small ? 14 : null,
|
||||
border: `1px solid ${fillColor}`,
|
||||
cursor: !disabled ? "pointer" : null,
|
||||
userSelect: "none",
|
||||
boxSizing: "border-box",
|
||||
marginRight,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
ref={hoverRef}
|
||||
style={{ ...styles.container, ...style }}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { BigButton } from "./BigButton";
|
||||
import React from "react";
|
||||
|
||||
export function ConfirmOrCancel({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
confirmTitle = "Confirm",
|
||||
leftBlock,
|
||||
hideCancel,
|
||||
disabled
|
||||
}) {
|
||||
const styles = {
|
||||
actions: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
cancel: {
|
||||
marginRight: 8,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.actions}>
|
||||
<div>{leftBlock}</div>
|
||||
<div>
|
||||
{!hideCancel ? (
|
||||
<BigButton
|
||||
title={"Cancel"}
|
||||
style={styles.cancel}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
) : null}
|
||||
<BigButton title={confirmTitle} inverted={true} onClick={onConfirm} disabled={disabled}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user