Compare commits

2 Commits

Author SHA1 Message Date
7aeaba7c12 Add EnvelopeGenerator.ReceiverUI project and UI components
Introduced a new Razor Components-based web application, `EnvelopeGenerator.ReceiverUI`, to the solution. This includes:

- Added `EnvelopeGenerator.ReceiverUI` project to `EnvelopeGenerator.sln` with Debug and Release configurations.
- Created `EnvelopeGenerator.ReceiverUI.csproj` targeting `.NET 8.0` with nullable and implicit usings enabled.
- Added Razor components (`App.razor`, `Routes.razor`, `MainLayout.razor`, `NavMenu.razor`, `Counter.razor`, `Error.razor`, `Home.razor`, `Weather.razor`) for layout, navigation, and pages.
- Included styles (`MainLayout.razor.css`, `NavMenu.razor.css`, `app.css`) and assets (`favicon.png`, `bootstrap.min.css`).
- Configured logging and allowed hosts in `appsettings.json` and `appsettings.Development.json`.
- Added `Program.cs` to configure and run the application.
- Added `launchSettings.json` for development and debugging profiles.

These changes enable a modern, interactive web interface with Bootstrap styling and development-friendly configurations.
2025-12-05 09:08:57 +01:00
2301a81a1c init receiver ui with react 2025-12-04 18:11:47 +01:00
134 changed files with 13657 additions and 2856 deletions

View File

@@ -74,11 +74,6 @@ public record EnvelopeDto
/// </summary>
public int? EnvelopeTypeId { get; set; }
/// <summary>
///
/// </summary>
public bool ReadOnly => EnvelopeTypeId == 2;
/// <summary>
///
/// </summary>

View File

@@ -5,7 +5,6 @@ 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>
@@ -18,7 +17,6 @@ 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;
@@ -35,7 +33,6 @@ 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;
@@ -50,33 +47,11 @@ 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>
@@ -93,13 +68,11 @@ 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();
}

View File

@@ -24,7 +24,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
/// <summary>
///
/// </summary>
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
/// <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>

View File

@@ -29,9 +29,6 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
{
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
}
public Task Handle(DocSignedNotification notification, CancellationToken cancel)
=> _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
}

View File

@@ -10,8 +10,6 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// </summary>
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{
private const string BlankAnnotationJson = "{}";
private readonly ISender _sender;
/// <summary>
@@ -35,9 +33,7 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{
Envelope = new() { Id = notification.EnvelopeId },
Receiver = new() { Id = notification.ReceiverId},
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson
Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
}, cancel);
}
}

View File

@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.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" />

View File

@@ -1,4 +1,4 @@
using AutoMapper;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Envelopes.Queries;
@@ -47,13 +47,7 @@ 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>>
{
/// <summary>
/// Optionaler Benutzernamefilter, um Ergebnisse auf Umschläge eines bestimmten Besitzers einzuschränken.
/// </summary>
public string? Username { get; init; }
}
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>;
/// <summary>
///
@@ -88,74 +82,73 @@ 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>
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
/// <param name="envelopeReceiver"></param>
/// <param name="mapper"></param>
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
{
private readonly IRepository<EnvelopeReceiver> _repo;
private readonly IRepository<Receiver> _rcvRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiver"></param>
/// <param name="rcvRepo"></param>
/// <param name="mapper"></param>
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
{
_repo = envelopeReceiver;
_mapper = mapper;
_rcvRepo = rcvRepo;
}
/// <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.Query.Where(request, notnull: false);
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);
}
_repo = envelopeReceiver;
_mapper = mapper;
_rcvRepo = rcvRepo;
}
}
/// <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)
{
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.Any())
{
var receiver = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
foreach (var envRcv in envRcvs)
envRcv.Envelope?.Documents?.First().Elements.RemoveAll(s => s.ReceiverId != receiver.Id);
}
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
}
}

View File

@@ -1,45 +0,0 @@
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);
}
}

View File

@@ -33,18 +33,7 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
/// <summary>
/// ID des Absenders
/// </summary>
internal int UserId { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public bool Authorize(int userId)
{
UserId = userId;
return true;
}
public int UserId { get; set; }
/// <summary>
/// Determines which component is used for envelope processing.

View File

@@ -1,33 +1,18 @@
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<IEnumerable<EnvelopeDto>>
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest
{
/// <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>
@@ -80,62 +65,4 @@ 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);
}
}

View File

@@ -1,8 +1,4 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Query;
using MediatR;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Application.Receivers.Queries;
namespace EnvelopeGenerator.Application.Envelopes.Queries;
@@ -10,54 +6,6 @@ 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() : ReceiverQueryBase, IRequest<string?>;
/// <summary>
///
/// </summary>
public class ReadReceiverNameQueryHandler : IRequestHandler<ReadReceiverNameQuery, string?>
public record ReadReceiverNameQuery() : ReadReceiverQuery
{
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;
}
}
}

View File

@@ -1,10 +1,4 @@
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;
using EnvelopeGenerator.Application.Common.Query;
namespace EnvelopeGenerator.Application.Receivers.Queries;
@@ -12,53 +6,4 @@ 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, 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);
}
}
public record ReadReceiverQuery : ReceiverQueryBase;

View File

@@ -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.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 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>
<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,6 +109,9 @@
<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>
@@ -463,12 +466,8 @@
<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">
@@ -496,9 +495,6 @@
<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">

View File

@@ -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.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.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" />

View File

@@ -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.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 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>
<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>

View File

@@ -422,7 +422,7 @@ Namespace Jobs
End Function
Private Function GetEnvelopeData(pEnvelopeId As Integer) As EnvelopeData
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID, T.ENVELOPE_TYPE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID,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)

View File

@@ -37,15 +37,6 @@ 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) _

View File

@@ -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.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.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" />

View File

@@ -2,7 +2,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using EnvelopeGenerator.Domain.Constants;
using Newtonsoft.Json;
#if NETFRAMEWORK
using System;
@@ -108,10 +107,6 @@ 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; }

View File

@@ -1,130 +0,0 @@
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);
}
}

View File

@@ -1,29 +0,0 @@
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);
}
}

View File

@@ -1,43 +0,0 @@
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");
}
}

View File

@@ -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 DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -23,9 +23,12 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
[Authorize]
public class EmailTemplateController : ControllerBase
{
private readonly ILogger<EmailTemplateController> _logger;
private readonly IMapper _mapper;
private readonly IRepository<EmailTemplate> _repository;
[Obsolete("Use IRepository")]
private readonly IEmailTemplateRepository _repository;
private readonly IMediator _mediator;
@@ -36,11 +39,12 @@ public class EmailTemplateController : ControllerBase
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
[Obsolete("Use MediatR")]
public EmailTemplateController(IMapper mapper, IRepository<EmailTemplate> repository, IMediator mediator)
[Obsolete("Use IRepository")]
public EmailTemplateController(IMapper mapper, IEmailTemplateRepository repository, ILogger<EmailTemplateController> logger, IMediator mediator)
{
_mapper = mapper;
_repository = repository;
_logger = logger;
_mediator = mediator;
}
@@ -63,7 +67,7 @@ public class EmailTemplateController : ControllerBase
{
if (emailTemplate is null || (emailTemplate.Id is null && emailTemplate.Type is null))
{
var temps = await _repository.Query.ToListAsync();
var temps = await _repository.ReadAllAsync();
return Ok(_mapper.Map<IEnumerable<EmailTemplateDto>>(temps));
}
else

View File

@@ -1,9 +1,11 @@
using EnvelopeGenerator.Application.Envelopes.Commands;
using DigitalData.Core.Abstraction.Application.DTO;
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;
@@ -27,16 +29,21 @@ 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>
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
[Obsolete("Use MediatR")]
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService, IMediator mediator)
{
_logger = logger;
_envelopeService = envelopeService;
_mediator = mediator;
}
@@ -52,56 +59,98 @@ 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)
{
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
return result.Any() ? Ok(result) : NotFound();
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);
}
}
/// <summary>
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
/// </summary>
/// <param name="query"></param>
/// <param name="id">Die eindeutige ID des Umschlags.</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")]
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetDocResultAsync([FromQuery] int id, [FromQuery] bool view = false)
{
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
var envelope = envelopes.FirstOrDefault();
if (User.GetId() is int intId)
return await _envelopeService.ReadByUserAsync(intId).ThenAsync(
Success: envelopes =>
{
var envelope = envelopes.Where(e => e.Id == id).FirstOrDefault();
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)
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
{
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
return File(envelope.DocResult, "application/pdf");
_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);
}
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
}
/// <summary>
///
/// </summary>
/// <param name="command"></param>
/// <param name="envelope"></param>
/// <returns></returns>
[NonAction]
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
public async Task<IActionResult> CreateAsync([FromQuery] CreateEnvelopeCommand envelope)
{
var res = await _mediator.Send(command.Authorize(User.GetId()));
envelope.UserId = User.GetId();
var res = await _mediator.Send(envelope);
if (res is null)
{
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(envelope));
return StatusCode(StatusCodes.Status500InternalServerError);
}
else

View File

@@ -1,4 +1,5 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Envelopes.Queries;
@@ -10,8 +11,12 @@ 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;
@@ -28,19 +33,38 @@ 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>
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
/// <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)
{
_logger = logger;
_erService = envelopeReceiverService;
_mediator = mediator;
_mapper = mapper;
_envelopeExecutor = envelopeExecutor;
@@ -63,6 +87,7 @@ 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();
@@ -74,11 +99,20 @@ public class EnvelopeReceiverController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError);
}
envelopeReceiver = envelopeReceiver with { Username = username };
var result = await _mediator.Send(envelopeReceiver);
return Ok(result);
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);
});
}
/// <summary>
@@ -95,17 +129,25 @@ 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)
{
var name = await _mediator.Send(receiver);
return name is null ? NotFound() : Ok(name);
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);
});
}
/// <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:
@@ -141,10 +183,13 @@ 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, CancellationToken cancel)
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request)
{
CancellationToken cancel = default;
int userId = User.GetId();
#region Create Envelope
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancel);
#endregion
#region Add receivers

View File

@@ -1,5 +1,5 @@
using MediatR;
using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -13,27 +13,36 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
public class EnvelopeTypeController : ControllerBase
{
private readonly ILogger<EnvelopeTypeController> _logger;
private readonly IMediator _mediator;
[Obsolete("Use MediatR")]
private readonly IEnvelopeTypeService _service;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="mediator"></param>
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IMediator mediator)
/// <param name="service"></param>
[Obsolete("Use MediatR")]
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IEnvelopeTypeService service)
{
_logger = logger;
_mediator = mediator;
_service = service;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[Obsolete("Use MediatR")]
[HttpGet]
public async Task<IActionResult> GetAllAsync()
{
var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
return Ok(result);
return await _service.ReadAllAsync().ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return ntc.HasFlag(Flag.NotFound) ? NotFound() : StatusCode(StatusCodes.Status500InternalServerError);
});
}
}

View File

@@ -1,95 +0,0 @@
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);
});
}
}

View File

@@ -1,7 +1,12 @@
using MediatR;
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.API;
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;
@@ -9,22 +14,22 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// Controller für die Verwaltung von Empfängern.
/// </summary>
/// <remarks>
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
/// 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.
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ReceiverController : ControllerBase
[Obsolete("Use MediatR")]
public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverService, CreateReceiverCommand, ReceiverDto, UpdateReceiverCommand, Receiver, int>
{
private readonly IMediator _mediator;
/// <summary>
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
/// </summary>
/// <param name="mediator">Mediator für Anfragen.</param>
public ReceiverController(IMediator mediator)
/// <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)
{
_mediator = mediator;
}
/// <summary>
@@ -35,13 +40,60 @@ public class ReceiverController : ControllerBase
[HttpGet]
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
{
if (!receiver.HasAnyCriteria)
{
var all = await _mediator.Send(new ReadReceiverQuery());
return Ok(all);
}
if (receiver.Id is null && receiver.EmailAddress is null && receiver.Signature is null)
return await base.GetAll();
var result = await _mediator.Send(receiver);
return result is null ? NotFound() : Ok(result);
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();
});
}
#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
}

View File

@@ -1,130 +0,0 @@
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() });
}
}
}

View File

@@ -1,18 +0,0 @@
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)}";
}
}

View File

@@ -1,87 +0,0 @@
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);
}
}

View File

@@ -1,14 +0,0 @@
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);
}

View File

@@ -1,60 +0,0 @@
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;
}
}

View File

@@ -1,17 +0,0 @@
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; }
}

View File

@@ -1,12 +0,0 @@
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();
}

View File

@@ -1,6 +0,0 @@
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();
}

View File

@@ -1,10 +0,0 @@
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";
}

View File

@@ -1,10 +0,0 @@
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;
}

View File

@@ -1,6 +0,0 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class MainViewModel
{
public string? Title { get; init; }
}

View File

@@ -1,92 +0,0 @@
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;
}
}
};

View File

@@ -1,79 +0,0 @@
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
}

View File

@@ -1,58 +0,0 @@
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;
}
}

View File

@@ -1,10 +0,0 @@
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;
}

View File

@@ -1,8 +0,0 @@
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;
}

View File

@@ -1,22 +0,0 @@
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; }
}

View File

@@ -1,17 +0,0 @@
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);
}

View File

@@ -12,7 +12,7 @@ namespace EnvelopeGenerator.Infrastructure
public EGDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.SetBasePath(Directory.GetCurrentDirectory()) // Önemli!
.AddJsonFile("appsettings.migration.json")
.Build();

View File

@@ -22,8 +22,8 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.5" />
<PackageReference Include="QuestPDF" Version="2025.7.1" />
</ItemGroup>

View File

@@ -1,22 +0,0 @@
<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>

View File

@@ -1,151 +0,0 @@
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");
}
}
}

View File

@@ -1,30 +0,0 @@
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;
}
}
}

View File

@@ -1,28 +0,0 @@
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) { }
}
}

View File

@@ -1,229 +0,0 @@
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);
}
}

View File

@@ -1,277 +0,0 @@
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
}

View File

@@ -1,19 +0,0 @@
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;
}

View File

@@ -1,46 +0,0 @@
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);
}
}
}

View File

@@ -1,91 +0,0 @@
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;
}
}

View File

@@ -1,19 +0,0 @@
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; }
}

View File

@@ -1,8 +0,0 @@
using System.Collections.Generic;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportSource
{
public List<ReportItem> Items { get; set; } = new();
}

View File

@@ -0,0 +1,20 @@
<!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>

View File

@@ -0,0 +1,23 @@
@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>

View File

@@ -0,0 +1,96 @@
.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;
}

View File

@@ -0,0 +1,30 @@
<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>

View File

@@ -0,0 +1,105 @@
.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;
}
}

View File

@@ -0,0 +1,19 @@
@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++;
}
}

View File

@@ -0,0 +1,36 @@
@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;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,64 @@
@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);
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,10 @@
@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

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,27 @@
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();

View File

@@ -0,0 +1,38 @@
{
"$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"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,51 @@
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.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -5,7 +5,6 @@ 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;
@@ -45,15 +44,9 @@ public class DocSignedNotificationTests : TestBase
// Create envelope receiver
var envRcv = this.CreateEnvelopeReceiver(env!.Id, rcv.Id);
var repo = GetRepository<EnvelopeReceiver>();
envRcv = await repo.CreateAsync(envRcv, cancel);
envRcv = await Repository.CreateAsync(envRcv, cancel);
var envRcvDto = _mapper.Map<EnvelopeReceiverDto>(envRcv);
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
var docSignedNtf = envRcvDto.ToDocSignedNotification(new () { });
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();

View File

@@ -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.6.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.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" />

View File

@@ -1,10 +1,8 @@
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;
@@ -13,7 +11,6 @@ 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;
@@ -23,6 +20,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Tests.Application;
@@ -44,29 +42,10 @@ public class Fake
// add Application and Infrastructure services
#pragma warning disable CS0618
services.AddEnvelopeGeneratorServices(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);
});
services.AddEnvelopeGeneratorInfrastructureServices(
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
context.Configuration
);
var prodCnnStr = context.Configuration.GetConnectionString("Default");
services.AddDbContext<EGDbContext2Prod>(opt => opt.UseSqlServer(prodCnnStr));

View File

@@ -24,8 +24,6 @@ public abstract class TestBase : Faker
protected IRepository Repository => Host.Services.GetRequiredService<IRepository>();
protected IServiceProvider Services => Host.Services;
protected abstract void ConfigureServices(IServiceCollection services);
[SetUp]
@@ -34,11 +32,9 @@ 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 repo.CreateAsync(temp);
await Repository.CreateAsync(temp);
}
[TearDown]

View File

@@ -1,17 +0,0 @@
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));
}
}

View File

@@ -0,0 +1,52 @@
{
"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.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,44 @@
<!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.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"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"
}

View File

@@ -0,0 +1,10 @@
body {
font-family: "Open Sans";
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}

View File

@@ -0,0 +1,278 @@
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;

View File

@@ -0,0 +1,8 @@
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();
});

View File

@@ -0,0 +1,37 @@
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>
);
}

View File

@@ -0,0 +1,15 @@
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>
}

View File

@@ -0,0 +1,77 @@
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>
}
/>
);
}

View File

@@ -0,0 +1,81 @@
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>
);
}

View File

@@ -0,0 +1,37 @@
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