Compare commits

65 Commits

Author SHA1 Message Date
d39018ca39 Add TfaRegistrationController for receiver TFA endpoints
Introduced TfaRegistrationController with endpoints to register and manage two-factor authentication for envelope receivers. Includes a GET endpoint to generate TFA registration metadata (QR code and deadline) and a POST endpoint to log out receivers. Implements error handling, logging, and uses dependency injection for required services. Added necessary using directives.
2026-01-30 15:05:32 +01:00
b49482137f Add ReadOnlyController for envelope sharing flows
Introduced ReadOnlyController to manage read-only envelope sharing. Added a POST endpoint for authorized users to create read-only receivers, send notification emails, and record sharing events in envelope history. Includes error handling and logging throughout the process.
2026-01-30 15:04:05 +01:00
bd40404d97 Add DocumentController for envelope document retrieval
Introduced DocumentController to provide a secured GET endpoint for authenticated receivers to download envelope documents. Handles missing or empty documents with error logging and NotFoundException. Utilizes MediatR and ILogger via dependency injection.
2026-01-30 14:48:10 +01:00
6f16921a79 Remove Obsolete attributes and update FirstAsync behavior
Removed Obsolete attributes from FirstAsync and Exceptions class. Changed FirstAsync return type to non-nullable Task<T> and updated its logic to throw the provided exception when the sequence is empty, instead of returning null.
2026-01-30 14:41:54 +01:00
1afc95f9c6 Add obsolete FirstAsync extension to TaskExtensions
Added FirstAsync<T, TException> as an obsolete extension method for Task<IEnumerable<T>>. This method returns the first element or throws a custom exception if the result is null, using a provided factory delegate. Intended for legacy .NET projects.
2026-01-30 14:24:57 +01:00
6aed820196 Mark TaskExtensions and new helpers as [Obsolete]
Marked TaskExtensions class and all its methods as [Obsolete] with guidance to implement Mediator behaviors instead. Added new [Obsolete] extension methods for null/empty checks and chaining. Introduced an [Obsolete] Exceptions class with factory methods for common exceptions. All changes are intended for legacy or transitional use only.
2026-01-30 14:20:05 +01:00
e17c4d02f8 Update Annotation model import to PsPdfKitAnnotation
Changed the import in ConfigController to use the PsPdfKitAnnotation namespace for annotation models instead of the previous Annotation namespace. This ensures the controller works with the updated annotation model definitions.
2026-01-30 13:07:00 +01:00
8187924a8c Add EnvelopeAuthExtensions for envelope claim handling
Introduces EnvelopeAuthExtensions with helper methods to retrieve envelope-specific claims from ClaimsPrincipal and to sign in envelope receivers using cookie authentication. Supports extracting envelope and receiver details via claims for authentication flows.
2026-01-30 13:06:40 +01:00
1bf530f7e7 Add EnvelopeClaimTypes for custom envelope claim strings
Introduced EnvelopeGenerator.GeneratorAPI namespace and EnvelopeClaimTypes static class. Added Title and Id claim type constants for envelope-related information, with XML documentation for clarity.
2026-01-30 13:06:27 +01:00
9cadc8e901 Add PSPDFKit annotation model and utilities
Introduce classes and interfaces for modeling PDF annotations, including support for layout, relative positioning, background rendering, and color. Added Annotation, AnnotationParams, Background, Color, Extensions, and IAnnotation to EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation. Enables flexible annotation management and rendering.
2026-01-30 13:02:34 +01:00
1d4ad13532 Add core model classes for auth, culture, images, and links
Introduced new models in EnvelopeGenerator.GeneratorAPI.Models:
- Auth, ContactLink, Culture, Cultures, CustomImages, ErrorViewModel, Image, MainViewModel, and TFARegParams.
These provide foundational structures for authentication, localization, error handling, image management, and contact links. All changes are new file additions.
2026-01-30 12:55:44 +01:00
03a8154b1c Add ConfigController to expose annotation config via API
Introduced a secured ConfigController with a GET endpoint at /api/Config/Annotations to provide annotation configuration data to clients. Utilizes dependency injection for configuration and includes necessary using directives.
2026-01-30 09:43:26 +01:00
20b8acd3fc Add AnnotationController for envelope annotation workflow
Introduces AnnotationController to manage envelope annotations and signature lifecycle. Includes endpoints for creating/updating annotations (for PSPDF Kit, obsolete) and rejecting documents, both requiring "FullyAuth" role. Utilizes MediatR for CQRS operations, dependency injection, and provides detailed logging and error handling. Legacy service dependencies are marked as obsolete.
2026-01-30 09:42:29 +01:00
a3afeb175f Refactor controllers for MediatR and cleaner API design
- Switch EnvelopeController and ReceiverController to MediatR for all operations
- Encapsulate UserId in CreateEnvelopeCommand via Authorize() method
- Change CreateEnvelopeCommand binding to [FromBody]
- Add CancellationToken support to EnvelopeReceiverController
- Remove obsolete CRUD logic from ReceiverController; now only supports GET via MediatR
- Clean up unused dependencies and update controller summaries for clarity
2026-01-28 14:14:04 +01:00
114555c843 Add XML docs to ReadEnvelopeReceiverQueryHandler ctor
Added XML documentation comments to the constructor of the ReadEnvelopeReceiverQueryHandler class, including parameter descriptions for envelopeReceiver, rcvRepo, and mapper. The summary section is currently empty.
2026-01-28 14:10:03 +01:00
f294ef2fde Refactor EnvelopeTypeController to use MediatR
Replace IEnvelopeTypeService with IMediator in EnvelopeTypeController and update GetAllAsync to use ReadEnvelopeTypesQuery. Remove obsolete service-based code and attributes. Also, remove AsNoTracking() from repository query in ReadEnvelopeTypesQueryHandler.
2026-01-28 13:48:02 +01:00
02ad819da9 Refactor EnvelopeReceiverController to use MediatR only
Remove all usage of IEnvelopeReceiverService from EnvelopeReceiverController, including constructor injection and obsolete attributes. Update endpoints to exclusively use MediatR for handling queries. Clean up related obsolete code, comments, and unused usings. Update documentation to reflect these changes.
2026-01-28 13:43:34 +01:00
041d98ca78 Refactor EnvelopeController to use MediatR exclusively
Removed all usage of the obsolete IEnvelopeService from EnvelopeController, refactoring endpoints to use MediatR with ReadEnvelopeQuery. Simplified GetAsync and GetDocResultAsync logic, updated method signatures, and eliminated obsolete code paths and attributes. Cleaned up unused usings and updated constructor dependencies.
2026-01-28 13:41:43 +01:00
afea2fb5ea Refactor envelope queries into unified ReadEnvelopeQuery
Consolidate envelope querying logic by extending ReadEnvelopeQuery to support user filtering and status options, and introduce ReadEnvelopeQueryHandler to process all envelope queries. Remove ReadUserEnvelopesQuery and its handler, reducing duplication and improving maintainability.
2026-01-28 13:34:54 +01:00
beeb9e4e75 Refactor EmailTemplateController to use generic IRepository
Replaces IEmailTemplateRepository with IRepository<EmailTemplate> in EmailTemplateController. Removes unused ILogger dependency and updates the Get method to use the new repository interface. Cleans up obsolete attributes and using directives.
2026-01-28 13:13:37 +01:00
30d13b1ffb Add MediatR query handler for reading receiver details
Introduced a CQRS-style ReadReceiverQueryHandler using MediatR to enable flexible querying of receivers by Id, EmailAddress, or Signature. Integrated AutoMapper for DTO mapping and updated ReadReceiverQuery to support MediatR's request/response pipeline.
2026-01-28 13:09:51 +01:00
814df63306 Add MediatR query/handler for reading envelope types
Introduced ReadEnvelopeTypesQuery and its handler to fetch all
envelope types from the repository, map them to DTOs, and
return the result via MediatR. This enables clean, decoupled
retrieval of envelope types in the application.
2026-01-28 12:55:44 +01:00
830d1af44a Add ReadUserEnvelopesQuery and handler with filtering
Implemented ReadUserEnvelopesQuery and its handler to fetch user envelopes with support for filtering by user ID, status (range, include, ignore), envelope ID, and UUID. Utilizes repository and AutoMapper to return EnvelopeDto results.
2026-01-28 12:54:22 +01:00
94018d2a36 Add query handler to fetch receiver's latest used name
Introduced ReadReceiverNameQueryHandler to retrieve the most recently used name ("Anrede") for a receiver, supporting envelope generation. Updated ReadReceiverNameQuery to use MediatR and CQRS patterns, and implemented repository-based lookups for receiver identification and name retrieval. Added necessary using directives for repository and MediatR support.
2026-01-28 12:51:40 +01:00
cf5a724bf2 Add username filter to ReadEnvelopeReceiverQuery handler
Extended ReadEnvelopeReceiverQuery to support optional username-based filtering. Refactored the handler to apply this filter, updated repository access for consistency, and improved code clarity and null-safety. Cleaned up comments and formatting.
2026-01-28 12:42:21 +01:00
172f2e27d7 Bump version to 3.9.0 in EnvelopeGenerator.Web.csproj
Updated the project, assembly, and file versions from 3.8.2 to 3.9.0 in the EnvelopeGenerator.Web.csproj file. This prepares the project for the next release.
2026-01-27 12:07:02 +01:00
d350e2ae48 Add envelope validation to BurnAnnotsToPDF function
Retrieve envelope by ID before burning annotations. Throw an exception if not found, and return the original PDF if the envelope is read-only. This adds validation and early exit logic before processing signatures and annotations.
2026-01-27 11:49:19 +01:00
2779452d72 Add ReadOnly property to Envelope class
Added a ReadOnly property to the Envelope class that returns true when EnvelopeTypeId is 2. The property is marked with [JsonIgnore] and [NotMapped] to exclude it from JSON serialization and database mapping.
2026-01-27 11:49:02 +01:00
5ebc6c6739 Update SQL to include ENVELOPE_TYPE in envelope query
The GetEnvelopeData function's SQL query now selects the ENVELOPE_TYPE column from the TBSIG_ENVELOPE table, allowing retrieval of envelope type information along with other envelope data.
2026-01-26 15:07:58 +01:00
891593755e Remove DLL ref, update configs, add WCF metadata
Removed EnvelopeGenerator.CommonServices DLL reference from BBTests project file, updated project GUIDs to lowercase, added WCFMetadata ItemGroup for connected services, and set BBTests Release config to use Debug in solution file.
2026-01-26 15:02:49 +01:00
b20260674e Improve PDF font handling; simplify report SQL fields
Enhanced PDFBurner to use a static FontProvider for better font support when rendering form field values. In ReportCreator, removed unused HEAD_TITLE and HEAD_SUBJECT fields from the SQL query and related mapping, streamlining report item loading.
2026-01-22 15:57:09 +01:00
7e5ff6bcb2 Refactor APIEnvelopeJob to use primary constructor
Simplifies APIEnvelopeJob by adopting C# primary constructor syntax.
Removes explicit constructors and initializes logger inline,
enabling direct dependency injection and reducing boilerplate.
2026-01-22 12:47:58 +01:00
6eed9b1e31 Update WorkerSettings with SQL Server connection string
Updated the ConnectionString property in both appsettings.json and appsettings.Development.json from an empty value to a specific SQL Server connection string, including server, database, credentials, and security options. This enables the application to connect to the designated database environment.
2026-01-22 10:14:44 +01:00
d4b1a4921c Refactor worker to use config, DI, and Quartz scheduling
- Add WorkerSettings class and update appsettings for config-driven setup
- Integrate Quartz.NET for job scheduling (FinalizeDocumentJob, APIEnvelopeJob)
- Refactor Program.cs for DI of services (TempFileManager, PDFBurner, etc.)
- Implement TempFileManager for temp folder management and cleanup
- Rewrite Worker class for config validation, DB check, and lifecycle logging
- Update csproj to include Quartz and EnvelopeGenerator.Jobs references
- Improve maintainability, error handling, and logging throughout
2026-01-22 09:51:35 +01:00
f078bafdde Refactor Jobs namespace and improve PDF handling
Refactored all EnvelopeGenerator.Jobs files to use the EnvelopeGenerator.Jobs namespace instead of EnvelopeGenerator.CommonServices.Jobs. Updated the .csproj to remove custom content and compile includes for the Jobs folder. Switched FinalizeDocumentJob to use dependency injection for PDFBurner, PDFMerger, and ReportCreator. Improved image annotation logic in PDFBurner for better placement and scaling, and refactored form field value rendering for conditional font styling. Aliased Document as LayoutDocument in ReportCreator to avoid ambiguity. Removed the obsolete Class1.cs file and made minor type safety improvements. These changes modernize the codebase and enhance maintainability.
2026-01-22 09:51:16 +01:00
786a3e128d Add EnvelopeGenerator.WorkerService as background worker
Added a new WorkerService project to the solution, targeting .NET 8.0 and using Microsoft.Extensions.Hosting. Implemented a Worker class that logs periodically, set up hosting in Program.cs, and included configuration files for logging and development. Updated the solution file to include and configure the new project.
2026-01-20 16:46:30 +01:00
ff3a146636 Add job framework for envelope processing and PDF finalization
Introduced new job classes for envelope processing and document finalization, including APIEnvelopeJob and FinalizeDocumentJob, both implementing Quartz IJob. Added supporting utilities for PDF annotation burning (PDFBurner), PDF merging (PDFMerger), and report generation (ReportCreator), along with related data models and exception types. Updated project references and dependencies to support Quartz scheduling, SQL Server access, and PDF manipulation with iText. This establishes a modular, extensible job-processing framework for envelope management and reporting.
2026-01-20 16:28:05 +01:00
40b2cad598 Add EnvelopeGenerator.Jobs project to the solution
Added new EnvelopeGenerator.Jobs project targeting .NET 8.0, with initial Class1.cs placeholder. Updated solution file to include the project with appropriate build configurations and solution folder nesting.
2026-01-20 15:52:41 +01:00
5c675be0ed Add type check to Handle method in AnnotationHandler
Refactored the Handle method to include a type check for PsPdfKitAnnotation before creating an annotation. This prevents errors when the notification does not contain the expected annotation type.
2026-01-20 13:57:52 +01:00
58164be640 Handle missing PsPdfKitAnnotation with blank JSON
Refactored DocStatusHandler to assign a blank JSON object ("{}")
to the Value property when PsPdfKitAnnotation is not present or
not of the expected type, preventing potential runtime errors.
2026-01-20 13:57:43 +01:00
a639377195 Make PsPdfKitAnnotation property and param nullable
Changed PsPdfKitAnnotation in DocSignedNotification to be nullable, removing the need for the null-forgiving operator. Updated ToDocSignedNotification to accept a nullable PsPdfKitAnnotation, ensuring consistency and allowing for cases where the annotation may be absent.
2026-01-20 13:57:29 +01:00
e3d6e87ee5 Allow nullable annotation param; validate for non-readonly
Make psPdfKitAnnotation optional in CreateOrUpdate. Add validation to require an annotation for non read-and-confirm envelopes, returning BadRequest if missing.
2026-01-20 12:11:49 +01:00
2795b91386 Refactor handleFinish to streamline READ_AND_CONFIRM flow and improve validation checks 2026-01-20 11:57:04 +01:00
ca248c3aa6 Support READ_AND_CONFIRM flow in handleFinish
Add logic to require all pages viewed before allowing finish when READ_AND_CONFIRM is enabled. Skip annotation and form field validations in this mode. Show warnings for unviewed pages and handle errors for save/sign actions. Update minified app to match new flow.
2026-01-20 11:44:40 +01:00
383634fca6 Conditionally add btn_refresh event based on READ_AND_CONFIRM
Only attach click handlers to btn_refresh elements when
READ_AND_CONFIRM is false. btn_complete and btn_reject
handlers remain unaffected. This change applies to both
app.js and app.min.js.
2026-01-20 11:11:59 +01:00
75097afa06 Add refresh button to envelope UI when not read-only
A "Refresh" button with a counterclockwise arrow icon is now shown in the envelope UI, but only when the envelope is not in read-only mode. The button uses standard styling classes for consistency.
2026-01-20 11:11:39 +01:00
77975c0644 Conditionally show "reset" button in mobile toolbar
The "reset" button in getMobileWritableItems is now only included if READ_AND_CONFIRM is falsy. This prevents the button from appearing when READ_AND_CONFIRM is true. The same conditional logic was applied to the minified ui.min.js. Code was also refactored for clarity.
2026-01-20 11:11:25 +01:00
5707213edd Conditionally apply PDF background for read-only envelopes
When preparing the PDF for the view, only apply the Background
method if the envelope is not read-only. This ensures that
read-only envelopes are displayed without additional background
elements.
2026-01-20 10:54:31 +01:00
ad54ba9dc4 Conditionally load annotations based on READ_AND_CONFIRM
Annotations are now only loaded if READ_AND_CONFIRM is falsy.
This prevents unnecessary annotation creation in read-and-confirm
scenarios. Changes applied to both app.js and app.min.js.
2026-01-20 10:54:16 +01:00
1f233153cf Restrict page view tracking to READ_AND_CONFIRM mode
Previously, page view tracking and sessionStorage updates ran unconditionally. Now, this logic is only executed when READ_AND_CONFIRM is enabled, ensuring viewed/unviewed page state is only tracked when required. Updated both source and minified files accordingly.
2026-01-20 10:38:37 +01:00
513ec007eb Set ViewData["ReadAndConfirm"] for envelope read-only state
Added "ReadAndConfirm" to ViewData, passing the envelope's ReadOnly property to the "ShowEnvelope" view. This enables the view to adjust its behavior or UI based on whether the envelope is read-only.
2026-01-20 10:30:25 +01:00
1305714da2 Move ReadOnly property from Envelope to EnvelopeDto
The ReadOnly property logic was shifted from the Envelope entity
to the EnvelopeDto record, ensuring that read-only status is
determined at the DTO layer rather than in the data model.
2026-01-20 10:30:07 +01:00
1e90cda393 Add READ_AND_CONFIRM JS constant from ViewData flag
Introduced a READ_AND_CONFIRM JavaScript constant in _Layout.cshtml, which reflects the server-side ViewData["ReadAndConfirm"] boolean value. This enables client-side scripts to easily check if the "ReadAndConfirm" flag is set.
2026-01-20 09:54:45 +01:00
5a5cbcb14d Track viewed PDF pages and persist state in sessionStorage
Added logic to monitor which PDF pages have been viewed by the user. The list of unviewed pages and a flag for all pages viewed are stored in sessionStorage and updated as the user navigates. Changes applied to both ui.js and ui.min.js.
2026-01-19 17:06:43 +01:00
a35f06070a Remove total page count logging after PSPDFKit load
Refactored ui.js and ui.min.js to eliminate retrieval and logging of the total page count after loading the PSPDFKit instance. The code now returns the instance directly after setting up the page change event listener, reducing unnecessary logging and simplifying the load process.
2026-01-19 17:03:05 +01:00
2606066103 Add logging for page changes and total pages in PSPDFKit
Added a .then() handler to loadPSPDFKit to log the active page number on page change events and log the total number of pages after loading. This aids in debugging and tracking user navigation within the document.
2026-01-19 16:57:34 +01:00
7495e062a9 Remove EnvelopeSigningType enum and update envelope logic
Removed EnvelopeSigningType enum and related normalization logic. Added a ReadOnly property to Envelope that uses EnvelopeTypeId to determine read-only status. Envelope type handling now relies on EnvelopeTypeId (int?) instead of the enum.
2026-01-19 16:50:02 +01:00
293044bec3 Handle envelope type with framework-specific properties
Add conditional logic to map "ENVELOPE_TYPE" to either an int? EnvelopeTypeId (for .NET Framework) or a type-safe EnvelopeSigningType SigningType (for .NET), supporting both legacy and modern approaches.
2026-01-19 16:21:58 +01:00
e0ff976d21 Add unit tests for EnvelopeSigningType.Normalize method
Added ConstantsTests to verify Normalize behavior for EnvelopeSigningType, including handling of valid and out-of-range enum values using NUnit test cases. Ensures normalization logic works as expected.
2026-01-19 16:08:39 +01:00
bec45ab1f1 Refactor test namespaces to EnvelopeGenerator.Tests.Application
All test files and utilities now use the EnvelopeGenerator.Tests.Application namespace for improved organization and clarity. No functional changes were made; updates are limited to namespaces and using directives. This makes it explicit that these are application-level tests and related helpers.
2026-01-19 16:02:39 +01:00
fecd054a5c Add EnvelopeSigningType enum and Normalize extension
Introduced EnvelopeSigningType enum with WetSignature and ReadAndSign values in the EnvelopeGenerator.Domain.Constants namespace. Added EnvelopeSigningTypeExtensions with a Normalize method to standardize enum values.
2026-01-19 16:01:13 +01:00
32b488c50f Refactor test dependency resolution in DocSignedNotificationTests
Refactored DocSignedNotificationTests to use typed repository
retrieval and explicit service resolution for PsPdfKitAnnotation.
Added a Services property to TestBase for easier access to
IServiceProvider. These changes improve clarity and robustness
of test dependency management.
2026-01-19 15:56:12 +01:00
9cfdd16970 Refactor test DB config to support SQL Server and fix seeding
Refactored Fake.cs to configure both in-memory and SQL Server
database contexts for testing, using the "Default" connection
string from configuration. Added detailed EF logging and SQL
executor setup. In TestBase.cs, fixed Setup to use the correct
repository instance for seeding email templates.
2026-01-19 15:51:09 +01:00
4da5848253 Refactor test namespaces; update package version
Changed test file namespaces from EnvelopeGenerator.Tests.Application to EnvelopeGenerator.Tests for consistency. Updated DigitalData.Core.Abstraction.Application package from 1.4.0 to 1.6.0 to incorporate latest improvements.
2026-01-19 15:14:59 +01:00
88da210ba2 Update DigitalData.Core.Abstraction.Application to 1.6.0
Updated DigitalData.Core.Abstraction.Application from 1.4.0 to 1.6.0 across all relevant project files and package configs. Also updated DigitalData.Core.Infrastructure from 2.4.5 to 2.6.1 in EnvelopeGenerator.Infrastructure.csproj. No other code changes were made.
2026-01-19 15:13:53 +01:00
134 changed files with 2849 additions and 13650 deletions

View File

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

View File

@@ -5,6 +5,7 @@ namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
/// Extension methods for tasks
/// </summary>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static class TaskExtensions
{
/// <summary>
@@ -17,6 +18,7 @@ public static class TaskExtensions
/// <param name="factory">Exception provider</param>
/// <returns>The awaited result if not <c>null</c>.</returns>
/// <exception>Thrown if the result is <c>null</c>.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception
{
var result = await task;
@@ -33,6 +35,7 @@ public static class TaskExtensions
/// <param name="factory">Exception provider</param>
/// <returns>The awaited collection if it is not <c>null</c> or empty.</returns>
/// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception
{
var result = await task;
@@ -47,11 +50,33 @@ public static class TaskExtensions
/// <param name="task"></param>
/// <param name="act"></param>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act)
{
var res = await task;
return act(res);
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="task"></param>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task) => task.Then(t => t.FirstOrDefault());
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TException"></typeparam>
/// <param name="task"></param>
/// <param name="factory"></param>
/// <returns></returns>
public static Task<T> FirstAsync<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory)
where TException : Exception
=> task.Then(t => t.FirstOrDefault() ?? throw factory());
}
/// <summary>
@@ -68,11 +93,13 @@ public static class Exceptions
///
/// </summary>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static BadRequestException BadRequest() => new();
/// <summary>
///
/// </summary>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static ForbiddenException Forbidden() => new();
}

View File

@@ -24,7 +24,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
/// <summary>
///
/// </summary>
public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
/// <summary>
///
@@ -59,7 +59,7 @@ public static class DocSignedNotificationExtensions
/// <param name="dtoTask"></param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns></returns>
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation psPdfKitAnnotation)
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
/// <summary>

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />

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,7 +47,13 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
/// Die Antwort enthält Details wie den Include, die Zuordnung zwischen Umschlag und Empfänger
/// sowie zusätzliche Metadaten.
/// </remarks>
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>;
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>
{
/// <summary>
/// Optionaler Benutzernamefilter, um Ergebnisse auf Umschläge eines bestimmten Besitzers einzuschränken.
/// </summary>
public string? Username { get; init; }
}
/// <summary>
///
@@ -82,73 +88,74 @@ public static class Extensions
q.Receiver.Signature = signature;
return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
}
}
/// <summary>
///
/// </summary>
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
{
private readonly IRepository<EnvelopeReceiver> _repo;
private readonly IRepository<Receiver> _rcvRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// Verarbeitet <see cref="ReadEnvelopeReceiverQuery"/> und liefert passende <see cref="EnvelopeReceiverDto"/>-Ergebnisse.
/// </summary>
/// <param name="envelopeReceiver"></param>
/// <param name="mapper"></param>
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
{
_repo = envelopeReceiver;
_mapper = mapper;
_rcvRepo = rcvRepo;
}
private readonly IRepository<EnvelopeReceiver> _repo;
private readonly IRepository<Receiver> _rcvRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
{
var q = _repo.ReadOnly().Where(request, notnull: false);
if (request.Envelope.Status is not null)
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiver"></param>
/// <param name="rcvRepo"></param>
/// <param name="mapper"></param>
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
{
var status = request.Envelope.Status;
if (status.Min is not null)
q = q.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
q = q.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Length > 0)
q = q.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
q = q.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
_repo = envelopeReceiver;
_mapper = mapper;
_rcvRepo = rcvRepo;
}
var envRcvs = await q
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
.Include(er => er.Envelope).ThenInclude(e => e!.User)
.Include(er => er.Receiver)
.ToListAsync(cancel);
if (request.Receiver.HasAnyCriteria && envRcvs.Any())
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
{
var receiver = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
var q = _repo.Query.Where(request, notnull: false);
foreach (var envRcv in envRcvs)
envRcv.Envelope?.Documents?.First().Elements.RemoveAll(s => s.ReceiverId != receiver.Id);
if (request.Username is string username)
q = q.Where(er => er.Envelope!.User.Username == username);
if (request.Envelope.Status is not null)
{
var status = request.Envelope.Status;
if (status.Min is not null)
q = q.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
q = q.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Length > 0)
q = q.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
q = q.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
}
var envRcvs = await q
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
.Include(er => er.Envelope).ThenInclude(e => e!.User)
.Include(er => er.Receiver)
.ToListAsync(cancel);
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0)
{
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel);
foreach (var envRcv in envRcvs)
envRcv.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id);
}
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
}
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
}
}
}

View File

@@ -0,0 +1,45 @@
using EnvelopeGenerator.Application.Common.Dto;
using MediatR;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.EnvelopeTypes.Queries;
/// <summary>
///
/// </summary>
public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
/// <summary>
///
/// </summary>
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
{
private readonly IRepository<EnvelopeType> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
{
var types = await _repository.Query.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
}
}

View File

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

View File

@@ -1,18 +1,33 @@
using MediatR;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Application.Common.Dto;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// <summary>
/// Repräsentiert eine Abfrage für Umschläge.
/// </summary>
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
{
/// <summary>
/// Abfrage des Include des Umschlags
/// </summary>
public EnvelopeStatusQuery? Status { get; init; }
/// <summary>
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
/// </summary>
public int? UserId { get; init; }
/// <summary>
/// Setzt den Benutzerkontext für die Abfrage.
/// </summary>
public ReadEnvelopeQuery Authorize(int userId) => this with { UserId = userId };
}
/// <summary>
@@ -65,4 +80,62 @@ public record EnvelopeStatusQuery
/// Eine Liste von Statuswerten, die ignoriert werden werden.
/// </summary>
public EnvelopeStatus[]? Ignore { get; init; }
}
/// <summary>
/// Verarbeitet <see cref="ReadEnvelopeQuery"/> und liefert passende <see cref="EnvelopeDto"/>-Ergebnisse.
/// </summary>
public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnumerable<EnvelopeDto>>
{
private readonly IRepository<Envelope> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadEnvelopeQueryHandler(IRepository<Envelope> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<IEnumerable<EnvelopeDto>> Handle(ReadEnvelopeQuery request, CancellationToken cancel)
{
var query = _repository.Query;
if (request.UserId is int userId)
query = query.Where(e => e.UserId == userId);
if (request.Id is int id)
query = query.Where(e => e.Id == id);
if (request.Uuid is string uuid)
query = query.Where(e => e.Uuid == uuid);
if (request.Status is { } status)
{
if (status.Min is not null)
query = query.Where(e => e.Status >= status.Min);
if (status.Max is not null)
query = query.Where(e => e.Status <= status.Max);
if (status.Include?.Length > 0)
query = query.Where(e => status.Include.Contains(e.Status));
if (status.Ignore?.Length > 0)
query = query.Where(e => !status.Ignore.Contains(e.Status));
}
var envelopes = await query
.Include(e => e.Documents)
.ToListAsync(cancel);
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
}
}

View File

@@ -1,4 +1,8 @@
using EnvelopeGenerator.Application.Receivers.Queries;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Query;
using MediatR;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Envelopes.Queries;
@@ -6,6 +10,54 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// Eine Abfrage, um die zuletzt verwendete Anrede eines Empfängers zu ermitteln,
/// damit diese für zukünftige Umschläge wiederverwendet werden kann.
/// </summary>
public record ReadReceiverNameQuery() : ReadReceiverQuery
public record ReadReceiverNameQuery() : ReceiverQueryBase, IRequest<string?>;
/// <summary>
///
/// </summary>
public class ReadReceiverNameQueryHandler : IRequestHandler<ReadReceiverNameQuery, string?>
{
}
private readonly IRepository<EnvelopeReceiver> _envelopeReceiverRepository;
private readonly IRepository<Receiver> _receiverRepository;
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiverRepository"></param>
/// <param name="receiverRepository"></param>
public ReadReceiverNameQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiverRepository, IRepository<Receiver> receiverRepository)
{
_envelopeReceiverRepository = envelopeReceiverRepository;
_receiverRepository = receiverRepository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string?> Handle(ReadReceiverNameQuery request, CancellationToken cancellationToken)
{
var receiverQuery = _receiverRepository.Query.AsNoTracking();
if (request.Id is int id)
receiverQuery = receiverQuery.Where(r => r.Id == id);
if (request.EmailAddress is string email)
receiverQuery = receiverQuery.Where(r => r.EmailAddress == email);
if (request.Signature is string signature)
receiverQuery = receiverQuery.Where(r => r.Signature == signature);
var receiver = await receiverQuery.FirstOrDefaultAsync(cancellationToken);
if (receiver is null)
return null;
var erName = await _envelopeReceiverRepository.Query
.Where(er => er.ReceiverId == receiver.Id)
.OrderByDescending(er => er.AddedWhen)
.Select(er => er.Name)
.FirstOrDefaultAsync(cancellationToken);
return erName;
}
}

View File

@@ -1,4 +1,10 @@
using EnvelopeGenerator.Application.Common.Query;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Query;
using MediatR;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Receivers.Queries;
@@ -6,4 +12,53 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
/// um spezifische Informationen über einen Empfänger abzurufen.
/// </summary>
public record ReadReceiverQuery : ReceiverQueryBase;
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
/// <summary>
///
/// </summary>
public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnumerable<ReceiverDto>>
{
private readonly IRepository<Receiver> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadReceiverQueryHandler(IRepository<Receiver> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IEnumerable<ReceiverDto>> Handle(ReadReceiverQuery request, CancellationToken cancellationToken)
{
var query = _repository.Query;
if (request.Id is int id)
{
query = query.Where(r => r.Id == id);
}
if (request.EmailAddress is string email)
{
query = query.Where(r => r.EmailAddress == email);
}
if (request.Signature is string signature)
{
query = query.Where(r => r.Signature == signature);
}
var receiver = await query.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<ReceiverDto>>(receiver);
}
}

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.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
@@ -109,9 +109,6 @@
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.SqlServer.dll</HintPath>
</Reference>
<Reference Include="EnvelopeGenerator.CommonServices">
<HintPath>..\EnvelopeGenerator.CommonServices\bin\Debug\EnvelopeGenerator.CommonServices.dll</HintPath>
</Reference>
<Reference Include="FirebirdSql.Data.FirebirdClient, Version=7.5.0.0, Culture=neutral, PublicKeyToken=3750abcc3150b00c, processorArchitecture=MSIL">
<HintPath>..\packages\FirebirdSql.Data.FirebirdClient.7.5.0\lib\net452\FirebirdSql.Data.FirebirdClient.dll</HintPath>
</Reference>
@@ -466,8 +463,12 @@
<Project>{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj">
<Project>{6ea0c51f-c2b1-4462-8198-3de0b32b74f8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj">
<Project>{4F32A98D-E6F0-4A09-BD97-1CF26107E837}</Project>
<Project>{4f32a98d-e6f0-4a09-bd97-1cf26107e837}</Project>
<Name>EnvelopeGenerator.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj">
@@ -495,6 +496,9 @@
<Content Include="MailLicense.xml" />
<Content Include="README.txt" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
<Import Project="..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets" Condition="Exists('..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

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.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

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.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>

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,T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID, T.ENVELOPE_TYPE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
WHERE T.GUID = {pEnvelopeId}"
Dim oTable As DataTable = Database.GetDatatable(oSql)

View File

@@ -37,6 +37,15 @@ Namespace Jobs.FinalizeDocument
Public Function BurnAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String), envelopeId As Integer) As Byte()
'read the elements of envelope with their annotations
Using scope = Factory.Shared.ScopeFactory.CreateScope()
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
If envelope Is Nothing Then
Throw New BurnAnnotationException($"Envelope with Id {envelopeId} not found.")
ElseIf envelope.ReadOnly Then
Return pSourceBuffer
End If
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _

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.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

View File

@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using EnvelopeGenerator.Domain.Constants;
using Newtonsoft.Json;
#if NETFRAMEWORK
using System;
@@ -107,6 +108,10 @@ public class Envelope
[Column("ENVELOPE_TYPE")]
public int? EnvelopeTypeId { get; set; }
[JsonIgnore]
[NotMapped]
public bool ReadOnly => EnvelopeTypeId == 2;
[Column("CERTIFICATION_TYPE")]
public int? CertificationType { get; set; }

View File

@@ -0,0 +1,130 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Manages annotations and signature lifecycle for envelopes.
/// </summary>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[ApiController]
[Route("api/[controller]")]
public class AnnotationController : ControllerBase
{
[Obsolete("Use MediatR")]
private readonly IEnvelopeHistoryService _historyService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverService _envelopeReceiverService;
private readonly IMediator _mediator;
private readonly ILogger<AnnotationController> _logger;
/// <summary>
/// Initializes a new instance of <see cref="AnnotationController"/>.
/// </summary>
[Obsolete("Use MediatR")]
public AnnotationController(
ILogger<AnnotationController> logger,
IEnvelopeHistoryService envelopeHistoryService,
IEnvelopeReceiverService envelopeReceiverService,
IMediator mediator)
{
_historyService = envelopeHistoryService;
_envelopeReceiverService = envelopeReceiverService;
_mediator = mediator;
_logger = logger;
}
/// <summary>
/// Creates or updates annotations for the authenticated envelope receiver.
/// </summary>
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
/// <param name="cancel">Cancellation token.</param>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost]
[Obsolete("This endpoint is for PSPDF Kit.")]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{
var signature = User.GetAuthReceiverSignature();
var uuid = User.GetAuthEnvelopeUuid();
if (signature is null || uuid is null)
{
_logger.LogError("Authorization failed: authenticated user does not have a valid signature or envelope UUID.");
return Unauthorized("User authentication is incomplete. Missing required claims for processing this request.");
}
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
return BadRequest();
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
return Problem(statusCode: StatusCodes.Status409Conflict);
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
return Problem(statusCode: StatusCodes.Status423Locked);
var docSignedNotification = await _mediator
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
.ToDocSignedNotification(psPdfKitAnnotation)
?? throw new NotFoundException("Envelope receiver is not found.");
await _mediator.PublishSafely(docSignedNotification, cancel);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
/// <summary>
/// Rejects the document for the current receiver.
/// </summary>
/// <param name="reason">Optional rejection reason.</param>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("reject")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> Reject([FromBody] string? reason = null)
{
var signature = User.GetAuthReceiverSignature();
var uuid = User.GetAuthEnvelopeUuid();
var mail = User.GetAuthReceiverMail();
if (uuid is null || signature is null || mail is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature,
message: @$"Unauthorized POST request in api\\envelope\\reject. One of claims, Envelope, signature or mail ({mail}) is null.");
return Unauthorized();
}
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
if (envRcvRes.IsFailed)
{
_logger.LogNotice(envRcvRes.Notices);
return Unauthorized("you are not authorized");
}
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
if (histRes.IsSuccess)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return NoContent();
}
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
_logger.LogNotice(histRes.Notices);
return StatusCode(500, histRes.Messages);
}
}

View File

@@ -0,0 +1,29 @@
using EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Exposes configuration data required by the client applications.
/// </summary>
/// <remarks>
/// Initializes a new instance of <see cref="ConfigController"/>.
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
{
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
/// <summary>
/// Returns annotation configuration that was previously rendered by MVC.
/// </summary>
[HttpGet("Annotations")]
public IActionResult GetAnnotationParams()
{
return Ok(_annotationParams.AnnotationJSObject);
}
}

View File

@@ -0,0 +1,43 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Provides access to envelope documents for authenticated receivers.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="DocumentController"/> class.
/// </remarks>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[ApiController]
[Route("api/[controller]")]
public class DocumentController(IMediator mediator, ILogger<DocumentController> logger) : ControllerBase
{
/// <summary>
/// Returns the document bytes for the specified envelope receiver key.
/// </summary>
/// <param name="query">Encoded envelope key.</param>
/// <param name="cancel">Cancellation token.</param>
[HttpGet]
public async Task<IActionResult> GetDocument(ReadEnvelopeReceiverQuery query, CancellationToken cancel)
{
var envRcv = await mediator.Send(query, cancel).FirstAsync(Exceptions.NotFound);
var byteData = envRcv.Envelope?.Documents?.FirstOrDefault()?.ByteData;
if (byteData is null || byteData.Length == 0)
{
logger.LogError("Document byte data is null or empty for envelope-receiver entity:\n{envelopeKey}.",
envRcv.ToJson(Format.Json.ForDiagnostics));
throw new NotFoundException("Document is empty.");
}
return File(byteData, "application/octet-stream");
}
}

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

View File

@@ -1,11 +1,9 @@
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Envelopes.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -29,21 +27,16 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
public class EnvelopeController : ControllerBase
{
private readonly ILogger<EnvelopeController> _logger;
[Obsolete("Use MediatR")]
private readonly IEnvelopeService _envelopeService;
private readonly IMediator _mediator;
/// <summary>
/// Erstellt eine neue Instanz des EnvelopeControllers.
/// </summary>
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
/// <param name="envelopeService">Der Dienst, der für die Verarbeitung von Umschlägen zuständig ist.</param>
/// <param name="mediator"></param>
[Obsolete("Use MediatR")]
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService, IMediator mediator)
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
{
_logger = logger;
_envelopeService = envelopeService;
_mediator = mediator;
}
@@ -59,98 +52,56 @@ public class EnvelopeController : ControllerBase
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[Authorize]
[HttpGet]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
{
if (User.GetId() is int intId)
return await _envelopeService.ReadByUserAsync(intId, min_status: envelope.Status?.Min, max_status: envelope.Status?.Max).ThenAsync(
Success: envelopes =>
{
if (envelope.Id is int id)
envelopes = envelopes.Where(e => e.Id == id);
if (envelope.Status is EnvelopeStatusQuery status)
envelopes = envelopes.Where(e => e.Status == status);
if (envelope.Uuid is string uuid)
envelopes = envelopes.Where(e => e.Uuid == uuid);
return envelopes.Any() ? Ok(envelopes) : NotFound();
},
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
else
{
_logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein.");
return StatusCode(StatusCodes.Status500InternalServerError);
}
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
return result.Any() ? Ok(result) : NotFound();
}
/// <summary>
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
/// </summary>
/// <param name="id">Die eindeutige ID des Umschlags.</param>
/// <param name="query"></param>
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[HttpGet("doc-result")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetDocResultAsync([FromQuery] int id, [FromQuery] bool view = false)
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
{
if (User.GetId() is int intId)
return await _envelopeService.ReadByUserAsync(intId).ThenAsync(
Success: envelopes =>
{
var envelope = envelopes.Where(e => e.Id == id).FirstOrDefault();
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
var envelope = envelopes.FirstOrDefault();
if (envelope is null)
return NotFound("Envelope not available.");
else if (envelope?.DocResult is null)
return NotFound("The document has not been fully signed or the result has not yet been released.");
else
{
if (view)
{
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
return File(envelope.DocResult, "application/pdf");
}
else
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
}
},
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
else
if (envelope is null)
return NotFound("Envelope not available.");
if (envelope.DocResult is null)
return NotFound("The document has not been fully signed or the result has not yet been released.");
if (view)
{
_logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein.");
return StatusCode(StatusCodes.Status500InternalServerError);
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
return File(envelope.DocResult, "application/pdf");
}
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
}
/// <summary>
///
/// </summary>
/// <param name="envelope"></param>
/// <param name="command"></param>
/// <returns></returns>
[NonAction]
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromQuery] CreateEnvelopeCommand envelope)
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
{
envelope.UserId = User.GetId();
var res = await _mediator.Send(envelope);
var res = await _mediator.Send(command.Authorize(User.GetId()));
if (res is null)
{
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(envelope));
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
return StatusCode(StatusCodes.Status500InternalServerError);
}
else

View File

@@ -1,5 +1,4 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Envelopes.Queries;
@@ -11,12 +10,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using System.Data;
using System.Reflection.Metadata;
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.SQL;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -33,38 +28,19 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
public class EnvelopeReceiverController : ControllerBase
{
private readonly ILogger<EnvelopeReceiverController> _logger;
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverService _erService;
private readonly IMediator _mediator;
private readonly IMapper _mapper;
private readonly IEnvelopeExecutor _envelopeExecutor;
private readonly IEnvelopeReceiverExecutor _erExecutor;
private readonly IDocumentExecutor _documentExecutor;
private readonly string _cnnStr;
/// <summary>
/// Konstruktor für den EnvelopeReceiverController.
/// </summary>
/// <param name="logger">Logger-Instanz zur Protokollierung von Informationen und Fehlern.</param>
/// <param name="envelopeReceiverService">Service zur Verwaltung von Umschlagempfängern.</param>
/// <param name="mediator">Mediator-Instanz zur Verarbeitung von Befehlen und Abfragen.</param>
/// <param name="mapper"></param>
/// <param name="envelopeExecutor"></param>
/// <param name="erExecutor"></param>
/// <param name="documentExecutor"></param>
/// <param name="csOpt"></param>
[Obsolete("Use MediatR")]
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
{
_logger = logger;
_erService = envelopeReceiverService;
_mediator = mediator;
_mapper = mapper;
_envelopeExecutor = envelopeExecutor;
@@ -87,7 +63,6 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[Authorize]
[HttpGet]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
{
var username = User.GetUsernameOrDefault();
@@ -99,20 +74,11 @@ public class EnvelopeReceiverController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError);
}
return await _erService.ReadByUsernameAsync(
username: username,
min_status: envelopeReceiver.Envelope.Status?.Min,
max_status: envelopeReceiver.Envelope.Status?.Max,
envelopeQuery: envelopeReceiver.Envelope,
receiverQuery: envelopeReceiver.Receiver,
ignore_statuses: envelopeReceiver.Envelope.Status?.Ignore ?? Array.Empty<EnvelopeStatus>())
.ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError, msg);
});
envelopeReceiver = envelopeReceiver with { Username = username };
var result = await _mediator.Send(envelopeReceiver);
return Ok(result);
}
/// <summary>
@@ -129,25 +95,17 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[Authorize]
[HttpGet("salute")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
{
return await _erService.ReadLastUsedReceiverNameByMailAsync(receiver.EmailAddress, receiver.Id, receiver.Signature).ThenAsync(
Success: res => res is null ? NotFound() : Ok(res),
Fail: IActionResult (msg, ntc) =>
{
if (ntc.HasFlag(Flag.NotFound))
return NotFound();
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
var name = await _mediator.Send(receiver);
return name is null ? NotFound() : Ok(name);
}
/// <summary>
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns>HTTP-Antwort</returns>
/// <remarks>
/// Sample request:
@@ -183,13 +141,10 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request)
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
{
CancellationToken cancel = default;
int userId = User.GetId();
#region Create Envelope
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancel);
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
#endregion
#region Add receivers

View File

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

View File

@@ -0,0 +1,95 @@
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Manages read-only envelope sharing flows.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class ReadOnlyController : ControllerBase
{
private readonly ILogger<ReadOnlyController> _logger;
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
private readonly IEnvelopeMailService _mailService;
private readonly IEnvelopeHistoryService _historyService;
/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyController"/> class.
/// </summary>
public ReadOnlyController(ILogger<ReadOnlyController> logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService)
{
_logger = logger;
_readOnlyService = readOnlyService;
_mailService = mailService;
_historyService = historyService;
}
/// <summary>
/// Creates a new read-only receiver for the current envelope.
/// </summary>
/// <param name="createDto">Creation payload.</param>
[HttpPost]
[Authorize(Roles = ReceiverRole.FullyAuth)]
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
{
var authReceiverMail = User.GetAuthReceiverMail();
if (authReceiverMail is null)
{
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
return Unauthorized();
}
var envelopeId = User.GetAuthEnvelopeId();
if (envelopeId is null)
{
_logger.LogError("Envelope Id claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
return Unauthorized();
}
createDto.AddedWho = authReceiverMail;
createDto.EnvelopeId = envelopeId;
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
if (creationRes.IsFailed)
{
_logger.LogNotice(creationRes);
return StatusCode(StatusCodes.Status500InternalServerError);
}
var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id);
if (readRes.IsFailed)
{
_logger.LogNotice(creationRes);
return StatusCode(StatusCodes.Status500InternalServerError);
}
var newReadOnly = readRes.Data;
return await _mailService.SendAsync(newReadOnly).ThenAsync<int, IActionResult>(SuccessAsync: async _ =>
{
var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared);
if (histRes.IsFailed)
{
_logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto));
_logger.LogNotice(histRes.Notices);
}
return Ok();
},
Fail: (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
}
}

View File

@@ -1,12 +1,7 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.API;
using MediatR;
using EnvelopeGenerator.Application.Receivers.Queries;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using EnvelopeGenerator.Application.Receivers.Commands;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -14,22 +9,22 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// Controller für die Verwaltung von Empfängern.
/// </summary>
/// <remarks>
/// Dieser Controller bietet Endpunkte für CRUD-Operationen (Erstellen, Lesen, Aktualisieren, Löschen)
/// sowie zusätzliche Funktionen wie das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
[Obsolete("Use MediatR")]
public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverService, CreateReceiverCommand, ReceiverDto, UpdateReceiverCommand, Receiver, int>
public class ReceiverController : ControllerBase
{
private readonly IMediator _mediator;
/// <summary>
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
/// </summary>
/// <param name="logger">Der Logger für die Protokollierung.</param>
/// <param name="service">Der Dienst für Empfängeroperationen.</param>
public ReceiverController(ILogger<ReceiverController> logger, IReceiverService service) : base(logger, service)
/// <param name="mediator">Mediator für Anfragen.</param>
public ReceiverController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
@@ -40,60 +35,13 @@ public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverS
[HttpGet]
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
{
if (receiver.Id is null && receiver.EmailAddress is null && receiver.Signature is null)
return await base.GetAll();
if (!receiver.HasAnyCriteria)
{
var all = await _mediator.Send(new ReadReceiverQuery());
return Ok(all);
}
if (receiver.Id is int id)
return await _service.ReadByIdAsync(id).ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
return NotFound();
});
return await _service.ReadByAsync(emailAddress: receiver.EmailAddress, signature: receiver.Signature).ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
return NotFound();
});
var result = await _mediator.Send(receiver);
return result is null ? NotFound() : Ok(result);
}
#region REMOVED ENDPOINTS
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> GetAll() => base.GetAll();
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Delete([FromRoute] int id) => base.Delete(id);
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Update(UpdateReceiverCommand updateDto) => base.Update(updateDto);
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Create(CreateReceiverCommand createDto)
{
return base.Create(createDto);
}
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> GetById([FromRoute] int id)
{
return base.GetById(id);
}
#endregion
}

View File

@@ -0,0 +1,130 @@
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using EnvelopeGenerator.GeneratorAPI.Models;
using Ganss.Xss;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
/// </summary>
[ApiController]
[Route("api/tfa")]
public class TfaRegistrationController : ControllerBase
{
private readonly ILogger<TfaRegistrationController> _logger;
private readonly IEnvelopeReceiverService _envelopeReceiverService;
private readonly IAuthenticator _authenticator;
private readonly IReceiverService _receiverService;
private readonly TFARegParams _parameters;
private readonly IStringLocalizer<Resource> _localizer;
/// <summary>
/// Initializes a new instance of the <see cref="TfaRegistrationController"/> class.
/// </summary>
public TfaRegistrationController(
ILogger<TfaRegistrationController> logger,
IEnvelopeReceiverService envelopeReceiverService,
IAuthenticator authenticator,
IReceiverService receiverService,
IOptions<TFARegParams> tfaRegParamsOptions,
IStringLocalizer<Resource> localizer)
{
_logger = logger;
_envelopeReceiverService = envelopeReceiverService;
_authenticator = authenticator;
_receiverService = receiverService;
_parameters = tfaRegParamsOptions.Value;
_localizer = localizer;
}
/// <summary>
/// Generates registration metadata (QR code and deadline) for a receiver.
/// </summary>
/// <param name="envelopeReceiverId">Encoded envelope receiver id.</param>
[Authorize]
[HttpGet("{envelopeReceiverId}")]
public async Task<IActionResult> RegisterAsync(string envelopeReceiverId)
{
try
{
var (uuid, signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
return Unauthorized(new { message = _localizer.WrongEnvelopeReceiverId() });
}
var secretResult = await _envelopeReceiverService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
if (secretResult.IsFailed)
{
_logger.LogNotice(secretResult.Notices);
return NotFound(new { message = _localizer.WrongEnvelopeReceiverId() });
}
var envelopeReceiver = secretResult.Data;
if (!envelopeReceiver.Envelope!.TFAEnabled)
return Unauthorized(new { message = _localizer.WrongAccessCode() });
var receiver = envelopeReceiver.Receiver;
receiver!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
await _receiverService.UpdateAsync(receiver);
var totpQr64 = _authenticator.GenerateTotpQrCode(userEmail: receiver.EmailAddress, secretKey: receiver.TotpSecretkey).ToBase64String();
if (receiver.TfaRegDeadline is null)
{
receiver.TfaRegDeadline = _parameters.Deadline;
await _receiverService.UpdateAsync(receiver);
}
else if (receiver.TfaRegDeadline <= DateTime.Now)
{
return StatusCode(StatusCodes.Status410Gone, new { message = _localizer.WrongAccessCode() });
}
return Ok(new
{
envelopeReceiver.EnvelopeId,
envelopeReceiver.Envelope!.Uuid,
envelopeReceiver.Receiver!.Signature,
receiver.TfaRegDeadline,
TotpQR64 = totpQr64
});
}
catch (Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer.WrongEnvelopeReceiverId());
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
}
}
/// <summary>
/// Logs out the envelope receiver from cookie authentication.
/// </summary>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("auth/logout")]
public async Task<IActionResult> LogOutAsync()
{
try
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "{message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
}
}
}

View File

@@ -0,0 +1,18 @@
namespace EnvelopeGenerator.GeneratorAPI
{
/// <summary>
/// Provides custom claim types for envelope-related information.
/// </summary>
public static class EnvelopeClaimTypes
{
/// <summary>
/// Claim type for the title of an envelope.
/// </summary>
public static readonly string Title = $"Envelope{nameof(Title)}";
/// <summary>
/// Claim type for the ID of an envelope.
/// </summary>
public static readonly string Id = $"Envelope{nameof(Id)}";
}
}

View File

@@ -0,0 +1,87 @@
using System.Security.Claims;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace EnvelopeGenerator.GeneratorAPI.Extensions;
/// <summary>
/// Provides helper methods for working with envelope-specific authentication claims.
/// </summary>
public static class EnvelopeAuthExtensions
{
/// <summary>
/// Retrieves a claim value by type.
/// </summary>
/// <param name="user">The current claims principal.</param>
/// <param name="claimType">The claim type to resolve.</param>
/// <returns>The claim value or null when missing.</returns>
public static string? GetClaimValue(this ClaimsPrincipal user, string claimType) => user.FindFirstValue(claimType);
/// <summary>
/// Gets the authenticated envelope UUID from the claims.
/// </summary>
public static string? GetAuthEnvelopeUuid(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.NameIdentifier);
/// <summary>
/// Gets the authenticated receiver signature from the claims.
/// </summary>
public static string? GetAuthReceiverSignature(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Hash);
/// <summary>
/// Gets the authenticated receiver display name from the claims.
/// </summary>
public static string? GetAuthReceiverName(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Name);
/// <summary>
/// Gets the authenticated receiver email address from the claims.
/// </summary>
public static string? GetAuthReceiverMail(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Email);
/// <summary>
/// Gets the authenticated envelope title from the claims.
/// </summary>
public static string? GetAuthEnvelopeTitle(this ClaimsPrincipal user) => user.FindFirstValue(EnvelopeClaimTypes.Title);
/// <summary>
/// Gets the authenticated envelope identifier from the claims.
/// </summary>
public static int? GetAuthEnvelopeId(this ClaimsPrincipal user)
{
var envIdStr = user.FindFirstValue(EnvelopeClaimTypes.Id);
return int.TryParse(envIdStr, out var envId) ? envId : null;
}
/// <summary>
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
/// </summary>
/// <param name="context">The current HTTP context.</param>
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
new(ClaimTypes.Role, receiverRole)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = false,
IsPersistent = false
};
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
}

View File

@@ -0,0 +1,14 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
{
public bool HasAccessCode => AccessCode is not null;
public bool HasSmsCode => SmsCode is not null;
public bool HasAuthenticatorCode => AuthenticatorCode is not null;
public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1;
public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode);
}

View File

@@ -0,0 +1,60 @@
namespace EnvelopeGenerator.GeneratorAPI.Models
{
/// <summary>
/// Represents a hyperlink for contact purposes with various HTML attributes.
/// </summary>
public class ContactLink
{
/// <summary>
/// Gets or sets the label of the hyperlink.
/// </summary>
public string Label { get; init; } = "Contact";
/// <summary>
/// Gets or sets the URL that the hyperlink points to.
/// </summary>
public string Href { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the target where the hyperlink should open.
/// Commonly used values are "_blank", "_self", "_parent", "_top".
/// </summary>
public string Target { get; set; } = "_blank";
/// <summary>
/// Gets or sets the relationship of the linked URL as space-separated link types.
/// Examples include "nofollow", "noopener", "noreferrer".
/// </summary>
public string Rel { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the filename that should be downloaded when clicking the hyperlink.
/// This attribute will only have an effect if the href attribute is set.
/// </summary>
public string Download { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the language of the linked resource. Useful when linking to
/// content in another language.
/// </summary>
public string HrefLang { get; set; } = "en";
/// <summary>
/// Gets or sets the MIME type of the linked URL. Helps browsers to handle
/// the type correctly when the link is clicked.
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// Gets or sets additional information about the hyperlink, typically viewed
/// as a tooltip when the mouse hovers over the link.
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets an identifier for the hyperlink, unique within the HTML document.
/// </summary>
public string Id { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,17 @@
using System.Globalization;
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class Culture
{
private string _language = string.Empty;
public string Language { get => _language;
init {
_language = value;
Info = new(value);
}
}
public string FIClass { get; init; } = string.Empty;
public CultureInfo? Info { get; init; }
}

View File

@@ -0,0 +1,12 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class Cultures : List<Culture>
{
public IEnumerable<string> Languages => this.Select(c => c.Language);
public IEnumerable<string> FIClasses => this.Select(c => c.FIClass);
public Culture Default => this.First();
public Culture? this[string? language] => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
}

View File

@@ -0,0 +1,6 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class CustomImages : Dictionary<string, Image>
{
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
}

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class ErrorViewModel
{
public string Title { get; init; } = "404";
public string Subtitle { get; init; } = "Hmmm...";
public string Body { get; init; } = "It looks like one of the developers fell asleep";
}

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class Image
{
public string Src { get; init; } = string.Empty;
public Dictionary<string, string> Classes { get; init; } = new();
public string GetClassIn(string page) => Classes.TryGetValue(page, out var cls) && cls is not null ? cls : string.Empty;
}

View File

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

View File

@@ -0,0 +1,92 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public record Annotation : IAnnotation
{
public required string Name { get; init; }
#region Bound Annotation
[JsonIgnore]
public string? HorBoundAnnotName { get; init; }
[JsonIgnore]
public string? VerBoundAnnotName { get; init; }
#endregion
#region Layout
[JsonIgnore]
public double? MarginLeft { get; set; }
[JsonIgnore]
public double MarginLeftRatio { get; init; } = 1;
[JsonIgnore]
public double? MarginTop { get; set; }
[JsonIgnore]
public double MarginTopRatio { get; init; } = 1;
public double? Width { get; set; }
[JsonIgnore]
public double WidthRatio { get; init; } = 1;
public double? Height { get; set; }
[JsonIgnore]
public double HeightRatio { get; init; } = 1;
#endregion
#region Position
public double Left => (MarginLeft ?? 0) + (HorBoundAnnot?.HorBoundary ?? 0);
public double Top => (MarginTop ?? 0) + (VerBoundAnnot?.VerBoundary ?? 0);
#endregion
#region Boundary
[JsonIgnore]
public double HorBoundary => Left + (Width ?? 0);
[JsonIgnore]
public double VerBoundary => Top + (Height ?? 0);
#endregion
#region BoundAnnot
[JsonIgnore]
public Annotation? HorBoundAnnot { get; set; }
[JsonIgnore]
public Annotation? VerBoundAnnot { get; set; }
#endregion
public Color? BackgroundColor { get; init; }
#region Border
public Color? BorderColor { get; init; }
public string? BorderStyle { get; init; }
public int? BorderWidth { get; set; }
#endregion
[JsonIgnore]
internal Annotation Default
{
set
{
// To set null value, annotation must have null (0) value but null must has non-null value
if (MarginLeft == null && value.MarginLeft != null)
MarginLeft = value.MarginLeft * MarginLeftRatio;
if (MarginTop == null && value.MarginTop != null)
MarginTop = value.MarginTop * MarginTopRatio;
if (Width == null && value.Width != null)
Width = value.Width * WidthRatio;
if (Height == null && value.Height != null)
Height = value.Height * HeightRatio;
}
}
};

View File

@@ -0,0 +1,79 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public class AnnotationParams
{
public AnnotationParams()
{
_AnnotationJSObjectInitor = new(CreateAnnotationJSObject);
}
public Background? Background { get; init; }
#region Annotation
[JsonIgnore]
public Annotation? DefaultAnnotation { get; init; }
private readonly List<Annotation> _annots = new List<Annotation>();
public bool TryGet(string name, out Annotation annotation)
{
#pragma warning disable CS8601 // Possible null reference assignment.
annotation = _annots.FirstOrDefault(a => a.Name == name);
#pragma warning restore CS8601 // Possible null reference assignment.
return annotation is not null;
}
public required IEnumerable<Annotation> Annotations
{
get => _annots;
init
{
_annots = value.ToList();
if (DefaultAnnotation is not null)
foreach (var annot in _annots)
annot.Default = DefaultAnnotation;
for (int i = 0; i < _annots.Count; i++)
{
#region set bound annotations
// horizontal
if (_annots[i].HorBoundAnnotName is string horBoundAnnotName)
if (TryGet(horBoundAnnotName, out var horBoundAnnot))
_annots[i].HorBoundAnnot = horBoundAnnot;
else
throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined.");
// vertical
if (_annots[i].VerBoundAnnotName is string verBoundAnnotName)
if (TryGet(verBoundAnnotName, out var verBoundAnnot))
_annots[i].VerBoundAnnot = verBoundAnnot;
else
throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined.");
#endregion
}
}
}
#endregion
#region AnnotationJSObject
private Dictionary<string, IAnnotation> CreateAnnotationJSObject()
{
var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation);
if (Background is not null)
{
Background.Locate(_annots);
dict.Add(Background.Name.ToLower(), Background);
}
return dict;
}
private readonly Lazy<Dictionary<string, IAnnotation>> _AnnotationJSObjectInitor;
public Dictionary<string, IAnnotation> AnnotationJSObject => _AnnotationJSObjectInitor.Value;
#endregion
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
/// <summary>
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
/// It is only the first annotation as a background for other annotations.
/// </summary>
public record Background : IAnnotation
{
[JsonIgnore]
public double Margin { get; init; }
public string Name { get; } = "Background";
public double? Width { get; set; }
public double? Height { get; set; }
public double Left { get; set; }
public double Top { get; set; }
public Color? BackgroundColor { get; init; }
#region Border
public Color? BorderColor { get; init; }
public string? BorderStyle { get; init; }
public int? BorderWidth { get; set; }
#endregion
public void Locate(IEnumerable<IAnnotation> annotations)
{
// set Top
if (annotations.MinBy(a => a.Top)?.Top is double minTop)
Top = minTop;
// set Left
if (annotations.MinBy(a => a.Left)?.Left is double minLeft)
Left = minLeft;
// set Width
if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight)
Width = maxRight - Left;
// set Height
if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom)
Height = maxBottom - Top;
// add margins
Top -= Margin;
Left -= Margin;
Width += Margin * 2;
Height += Margin * 2;
}
}

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public record Color
{
public int R { get; init; } = 0;
public int G { get; init; } = 0;
public int B { get; init; } = 0;
}

View File

@@ -0,0 +1,8 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public static class Extensions
{
public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0;
public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0;
}

View File

@@ -0,0 +1,22 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public interface IAnnotation
{
string Name { get; }
double? Width { get; }
double? Height { get; }
double Left { get; }
double Top { get; }
Color? BackgroundColor { get; }
Color? BorderColor { get; }
string? BorderStyle { get; }
int? BorderWidth { get; }
}

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
/// <summary>
/// Represents the parameters for two-factor authentication (2FA) registration.
/// </summary>
public class TFARegParams
{
/// <summary>
/// The maximum allowed time for completing the registration process.
/// </summary>
public TimeSpan TimeLimit { get; init; } = new(0, 30, 0);
/// <summary>
/// The deadline for registration, calculated as the current time plus the <see cref="TimeLimit"/>.
/// </summary>
public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks);
}

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Quartz" Version="3.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Quartz;
namespace EnvelopeGenerator.Jobs.APIBackendJobs;
public class APIEnvelopeJob(ILogger<APIEnvelopeJob>? logger = null) : IJob
{
private readonly ILogger<APIEnvelopeJob> _logger = logger ?? NullLogger<APIEnvelopeJob>.Instance;
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("API Envelopes - Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("API Envelopes - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
await ProcessInvitationsAsync(connection, context.CancellationToken);
await ProcessWithdrawnAsync(connection, context.CancellationToken);
_logger.LogDebug("API Envelopes - Completed job {JobId} successfully", jobId);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "API Envelopes job failed");
}
finally
{
_logger.LogDebug("API Envelopes execution for {JobId} ended", jobId);
}
}
private async Task ProcessInvitationsAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE SOURCE = 'API' AND STATUS = 1003 ORDER BY GUID";
var envelopeIds = new List<int>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
if (reader[0] is int id)
{
envelopeIds.Add(id);
}
}
}
if (envelopeIds.Count == 0)
{
_logger.LogDebug("SendInvMail - No envelopes found");
return;
}
_logger.LogInformation("SendInvMail - Found {Count} envelopes", envelopeIds.Count);
var total = envelopeIds.Count;
var current = 1;
foreach (var id in envelopeIds)
{
_logger.LogInformation("SendInvMail - Processing Envelope {EnvelopeId} ({Current}/{Total})", id, current, total);
try
{
// Placeholder for invitation email sending logic.
_logger.LogDebug("SendInvMail - Marking envelope {EnvelopeId} as queued", id);
const string updateSql = "UPDATE TBSIG_ENVELOPE SET CURRENT_WORK_APP = @App WHERE GUID = @Id";
await using var updateCommand = new SqlCommand(updateSql, connection);
updateCommand.Parameters.AddWithValue("@App", "signFLOW_API_EnvJob_InvMail");
updateCommand.Parameters.AddWithValue("@Id", id);
await updateCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "SendInvMail - Unhandled exception while working envelope {EnvelopeId}", id);
}
current++;
_logger.LogInformation("SendInvMail - Envelope finalized");
}
}
private async Task ProcessWithdrawnAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = @"SELECT ENV.GUID, REJ.COMMENT AS REJECTION_REASON FROM
(SELECT * FROM TBSIG_ENVELOPE WHERE STATUS = 1009 AND SOURCE = 'API') ENV INNER JOIN
(SELECT MAX(GUID) GUID, ENVELOPE_ID, MAX(ADDED_WHEN) ADDED_WHEN, MAX(ACTION_DATE) ACTION_DATE, COMMENT FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 1009 GROUP BY ENVELOPE_ID, COMMENT ) REJ ON ENV.GUID = REJ.ENVELOPE_ID LEFT JOIN
(SELECT * FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 3004 ) M_Send ON ENV.GUID = M_Send.ENVELOPE_ID
WHERE M_Send.GUID IS NULL";
var withdrawn = new List<(int EnvelopeId, string Reason)>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
var id = reader.GetInt32(0);
var reason = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
withdrawn.Add((id, reason));
}
}
if (withdrawn.Count == 0)
{
_logger.LogDebug("WithdrawnEnv - No envelopes found");
return;
}
_logger.LogInformation("WithdrawnEnv - Found {Count} envelopes", withdrawn.Count);
var total = withdrawn.Count;
var current = 1;
foreach (var (envelopeId, reason) in withdrawn)
{
_logger.LogInformation("WithdrawnEnv - Processing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
// Log withdrawn mail trigger placeholder
const string insertHistory = "INSERT INTO TBSIG_ENVELOPE_HISTORY (ENVELOPE_ID, STATUS, USER_REFERENCE, ADDED_WHEN, ACTION_DATE, COMMENT) VALUES (@EnvelopeId, @Status, @UserReference, GETDATE(), GETDATE(), @Comment)";
await using var insertCommand = new SqlCommand(insertHistory, connection);
insertCommand.Parameters.AddWithValue("@EnvelopeId", envelopeId);
insertCommand.Parameters.AddWithValue("@Status", 3004);
insertCommand.Parameters.AddWithValue("@UserReference", "API");
insertCommand.Parameters.AddWithValue("@Comment", reason ?? string.Empty);
await insertCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "WithdrawnEnv - Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("WithdrawnEnv - Envelope finalized");
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Data;
namespace EnvelopeGenerator.Jobs;
public static class DataRowExtensions
{
public static T? GetValueOrDefault<T>(this DataRow row, string columnName, T? defaultValue = default)
{
if (!row.Table.Columns.Contains(columnName))
{
return defaultValue;
}
var value = row[columnName];
if (value == DBNull.Value)
{
return defaultValue;
}
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
}

View File

@@ -0,0 +1,28 @@
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public static class FinalizeDocumentExceptions
{
public class MergeDocumentException : ApplicationException
{
public MergeDocumentException(string message) : base(message) { }
public MergeDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
public class BurnAnnotationException : ApplicationException
{
public BurnAnnotationException(string message) : base(message) { }
public BurnAnnotationException(string message, Exception innerException) : base(message, innerException) { }
}
public class CreateReportException : ApplicationException
{
public CreateReportException(string message) : base(message) { }
public CreateReportException(string message, Exception innerException) : base(message, innerException) { }
}
public class ExportDocumentException : ApplicationException
{
public ExportDocumentException(string message) : base(message) { }
public ExportDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@@ -0,0 +1,229 @@
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Quartz;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class FinalizeDocumentJob : IJob
{
private readonly ILogger<FinalizeDocumentJob> _logger;
private readonly PDFBurner _pdfBurner;
private readonly PDFMerger _pdfMerger;
private readonly ReportCreator _reportCreator;
private record ConfigSettings(string DocumentPath, string DocumentPathOrigin, string ExportPath);
public FinalizeDocumentJob(
ILogger<FinalizeDocumentJob> logger,
PDFBurner pdfBurner,
PDFMerger pdfMerger,
ReportCreator reportCreator)
{
_logger = logger;
_pdfBurner = pdfBurner;
_pdfMerger = pdfMerger;
_reportCreator = reportCreator;
}
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("FinalizeDocument - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
var config = await LoadConfigurationAsync(connection, context.CancellationToken);
var envelopes = await LoadCompletedEnvelopesAsync(connection, context.CancellationToken);
if (envelopes.Count == 0)
{
_logger.LogInformation("No completed envelopes found");
return;
}
var total = envelopes.Count;
var current = 1;
foreach (var envelopeId in envelopes)
{
_logger.LogInformation("Finalizing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
var envelopeData = await GetEnvelopeDataAsync(connection, envelopeId, context.CancellationToken);
if (envelopeData is null)
{
_logger.LogWarning("Envelope data not found for {EnvelopeId}", envelopeId);
continue;
}
var data = envelopeData.Value;
var envelope = new Envelope
{
Id = envelopeId,
Uuid = data.EnvelopeUuid ?? string.Empty,
Title = data.Title ?? string.Empty,
FinalEmailToCreator = (int)FinalEmailType.No,
FinalEmailToReceivers = (int)FinalEmailType.No
};
var burned = _pdfBurner.BurnAnnotsToPDF(data.DocumentBytes, data.AnnotationData, envelopeId);
var report = _reportCreator.CreateReport(connection, envelope);
var merged = _pdfMerger.MergeDocuments(burned, report);
var outputDirectory = Path.Combine(config.ExportPath, data.ParentFolderUid);
Directory.CreateDirectory(outputDirectory);
var outputPath = Path.Combine(outputDirectory, $"{envelope.Uuid}.pdf");
await File.WriteAllBytesAsync(outputPath, merged, context.CancellationToken);
await UpdateDocumentResultAsync(connection, envelopeId, merged, context.CancellationToken);
await ArchiveEnvelopeAsync(connection, envelopeId, context.CancellationToken);
}
catch (MergeDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at merging documents");
}
catch (ExportDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at exporting document");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("Envelope {EnvelopeId} finalized", envelopeId);
}
_logger.LogDebug("Completed job {JobId} successfully", jobId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Certificate Document job failed");
}
finally
{
_logger.LogDebug("Job execution for {JobId} ended", jobId);
}
}
private async Task<ConfigSettings> LoadConfigurationAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT TOP 1 DOCUMENT_PATH, EXPORT_PATH FROM TBSIG_CONFIG";
await using var command = new SqlCommand(sql, connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (await reader.ReadAsync(cancellationToken))
{
var documentPath = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
var exportPath = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
return new ConfigSettings(documentPath, documentPath, exportPath);
}
return new ConfigSettings(string.Empty, string.Empty, Path.GetTempPath());
}
private async Task<List<int>> LoadCompletedEnvelopesAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE STATUS = @Status AND DATEDIFF(minute, CHANGED_WHEN, GETDATE()) >= 1 ORDER BY GUID";
var ids = new List<int>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeCompletelySigned);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
ids.Add(reader.GetInt32(0));
}
return ids;
}
private async Task<(int EnvelopeId, string? EnvelopeUuid, string? Title, byte[] DocumentBytes, List<string> AnnotationData, string ParentFolderUid)?> GetEnvelopeDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = @"SELECT T.GUID, T.ENVELOPE_UUID, T.TITLE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
WHERE T.GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
{
return null;
}
var envelopeUuid = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
var title = reader.IsDBNull(2) ? string.Empty : reader.GetString(2);
var filePath = reader.IsDBNull(3) ? string.Empty : reader.GetString(3);
var bytes = reader.IsDBNull(4) ? Array.Empty<byte>() : (byte[])reader[4];
await reader.CloseAsync();
if (bytes.Length == 0 && !string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
{
bytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
}
var annotations = await GetAnnotationDataAsync(connection, envelopeId, cancellationToken);
var parentFolderUid = !string.IsNullOrWhiteSpace(filePath)
? Path.GetFileName(Path.GetDirectoryName(filePath) ?? string.Empty)
: envelopeUuid;
return (envelopeId, envelopeUuid, title, bytes, annotations, parentFolderUid ?? envelopeUuid ?? envelopeId.ToString());
}
private async Task<List<string>> GetAnnotationDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<string>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
if (!reader.IsDBNull(0))
{
result.Add(reader.GetString(0));
}
}
return result;
}
private static async Task UpdateDocumentResultAsync(SqlConnection connection, int envelopeId, byte[] bytes, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET DOC_RESULT = @ImageData WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@ImageData", bytes);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
private static async Task ArchiveEnvelopeAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET STATUS = @Status, CHANGED_WHEN = GETDATE() WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeArchived);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,277 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using iText.IO.Image;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Font;
using iText.Layout.Properties;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutImage = iText.Layout.Element.Image;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurner
{
private static readonly FontProvider FontProvider = CreateFontProvider();
private readonly ILogger<PDFBurner> _logger;
private readonly PDFBurnerParams _pdfBurnerParams;
public PDFBurner() : this(NullLogger<PDFBurner>.Instance, new PDFBurnerParams())
{
}
public PDFBurner(ILogger<PDFBurner> logger, PDFBurnerParams? pdfBurnerParams = null)
{
_logger = logger;
_pdfBurnerParams = pdfBurnerParams ?? new PDFBurnerParams();
}
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, IList<string> instantJsonList, int envelopeId)
{
if (sourceBuffer is null || sourceBuffer.Length == 0)
{
throw new BurnAnnotationException("Source document is empty");
}
try
{
using var inputStream = new MemoryStream(sourceBuffer);
using var outputStream = new MemoryStream();
using var reader = new PdfReader(inputStream);
using var writer = new PdfWriter(outputStream);
using var pdf = new PdfDocument(reader, writer);
foreach (var json in instantJsonList ?? Enumerable.Empty<string>())
{
if (string.IsNullOrWhiteSpace(json))
{
continue;
}
var annotationData = JsonConvert.DeserializeObject<AnnotationData>(json);
if (annotationData?.annotations is null)
{
continue;
}
annotationData.annotations.Reverse();
foreach (var annotation in annotationData.annotations)
{
try
{
switch (annotation.type)
{
case AnnotationType.Image:
AddImageAnnotation(pdf, annotation, annotationData.attachments);
break;
case AnnotationType.Ink:
AddInkAnnotation(pdf, annotation);
break;
case AnnotationType.Widget:
var formFieldValue = annotationData.formFieldValues?.FirstOrDefault(fv => fv.name == annotation.id);
if (formFieldValue is not null && !_pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value))
{
AddFormFieldValue(pdf, annotation, formFieldValue.value);
}
break;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error applying annotation {AnnotationId} on envelope {EnvelopeId}", annotation.id, envelopeId);
throw new BurnAnnotationException("Adding annotation failed", ex);
}
}
}
pdf.Close();
return outputStream.ToArray();
}
catch (BurnAnnotationException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to burn annotations for envelope {EnvelopeId}", envelopeId);
throw new BurnAnnotationException("Annotations could not be burned", ex);
}
}
private void AddImageAnnotation(PdfDocument pdf, Annotation annotation, Dictionary<string, Attachment>? attachments)
{
if (attachments is null || string.IsNullOrWhiteSpace(annotation.imageAttachmentId) || !attachments.TryGetValue(annotation.imageAttachmentId, out var attachment))
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var imageBytes = Convert.FromBase64String(attachment.binary);
var imageData = ImageDataFactory.Create(imageBytes);
var image = new LayoutImage(imageData)
.ScaleAbsolute(width, height)
.SetFixedPosition(annotation.pageIndex + 1, x, y);
using var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.Add(image);
}
private void AddInkAnnotation(PdfDocument pdf, Annotation annotation)
{
if (annotation.lines?.points is null)
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new PdfCanvas(page);
var color = ParseColor(annotation.strokeColor);
canvas.SetStrokeColor(color);
canvas.SetLineWidth(1);
foreach (var segment in annotation.lines.points)
{
var first = true;
foreach (var point in segment)
{
var (px, py) = (ToInches(point[0]), ToInches(point[1]));
if (first)
{
canvas.MoveTo(px, py);
first = false;
}
else
{
canvas.LineTo(px, py);
}
}
canvas.Stroke();
}
}
private static FontProvider CreateFontProvider()
{
var provider = new FontProvider();
provider.AddStandardPdfFonts();
provider.AddSystemFonts();
return provider;
}
private void AddFormFieldValue(PdfDocument pdf, Annotation annotation, string value)
{
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.SetProperty(Property.FONT_PROVIDER, FontProvider);
canvas.SetProperty(Property.FONT, FontProvider.GetFontSet());
var paragraph = new Paragraph(value)
.SetFontSize(_pdfBurnerParams.FontSize)
.SetFontColor(ColorConstants.BLACK)
.SetFontFamily(_pdfBurnerParams.FontName);
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Italic))
{
paragraph.SetItalic();
}
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Bold))
{
paragraph.SetBold();
}
canvas.ShowTextAligned(
paragraph,
x + (float)_pdfBurnerParams.TopMargin,
y + (float)_pdfBurnerParams.YOffset,
annotation.pageIndex + 1,
iText.Layout.Properties.TextAlignment.LEFT,
iText.Layout.Properties.VerticalAlignment.TOP,
0);
}
private static DeviceRgb ParseColor(string? color)
{
if (string.IsNullOrWhiteSpace(color))
{
return new DeviceRgb(0, 0, 0);
}
try
{
var drawingColor = ColorTranslator.FromHtml(color);
return new DeviceRgb(drawingColor.R, drawingColor.G, drawingColor.B);
}
catch
{
return new DeviceRgb(0, 0, 0);
}
}
private static double ToInches(double value) => value / 72d;
private static double ToInches(float value) => value / 72d;
#region Model
private static class AnnotationType
{
public const string Image = "pspdfkit/image";
public const string Ink = "pspdfkit/ink";
public const string Widget = "pspdfkit/widget";
}
private sealed class AnnotationData
{
public List<Annotation>? annotations { get; set; }
public Dictionary<string, Attachment>? attachments { get; set; }
public List<FormFieldValue>? formFieldValues { get; set; }
}
private sealed class Annotation
{
public string id { get; set; } = string.Empty;
public List<double> bbox { get; set; } = new();
public string type { get; set; } = string.Empty;
public string imageAttachmentId { get; set; } = string.Empty;
public Lines? lines { get; set; }
public int pageIndex { get; set; }
public string strokeColor { get; set; } = string.Empty;
public string egName { get; set; } = string.Empty;
}
private sealed class Lines
{
public List<List<List<float>>> points { get; set; } = new();
}
private sealed class Attachment
{
public string binary { get; set; } = string.Empty;
public string contentType { get; set; } = string.Empty;
}
private sealed class FormFieldValue
{
public string name { get; set; } = string.Empty;
public string value { get; set; } = string.Empty;
}
#endregion
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Drawing;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurnerParams
{
public List<string> IgnoredLabels { get; } = new() { "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" };
public double TopMargin { get; set; } = 0.1;
public double YOffset { get; set; } = -0.3;
public string FontName { get; set; } = "Arial";
public int FontSize { get; set; } = 8;
public FontStyle FontStyle { get; set; } = FontStyle.Italic;
}

View File

@@ -0,0 +1,46 @@
using System.IO;
using iText.Kernel.Pdf;
using iText.Kernel.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFMerger
{
private readonly ILogger<PDFMerger> _logger;
public PDFMerger() : this(NullLogger<PDFMerger>.Instance)
{
}
public PDFMerger(ILogger<PDFMerger> logger)
{
_logger = logger;
}
public byte[] MergeDocuments(byte[] document, byte[] report)
{
try
{
using var finalStream = new MemoryStream();
using var documentReader = new PdfReader(new MemoryStream(document));
using var reportReader = new PdfReader(new MemoryStream(report));
using var writer = new PdfWriter(finalStream);
using var targetDoc = new PdfDocument(documentReader, writer);
using var reportDoc = new PdfDocument(reportReader);
var merger = new PdfMerger(targetDoc);
merger.Merge(reportDoc, 1, reportDoc.GetNumberOfPages());
targetDoc.Close();
return finalStream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to merge PDF documents");
throw new MergeDocumentException("Documents could not be merged", ex);
}
}
}

View File

@@ -0,0 +1,91 @@
using System.Data;
using System.IO;
using EnvelopeGenerator.Domain.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutDocument = iText.Layout.Document;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportCreator
{
private readonly ILogger<ReportCreator> _logger;
public ReportCreator() : this(NullLogger<ReportCreator>.Instance)
{
}
public ReportCreator(ILogger<ReportCreator> logger)
{
_logger = logger;
}
public byte[] CreateReport(SqlConnection connection, Envelope envelope)
{
try
{
var reportItems = LoadReportItems(connection, envelope.Id);
using var stream = new MemoryStream();
using var writer = new PdfWriter(stream);
using var pdf = new PdfDocument(writer);
using var document = new LayoutDocument(pdf);
document.Add(new Paragraph("Envelope Finalization Report").SetFontSize(16));
document.Add(new Paragraph($"Envelope Id: {envelope.Id}"));
document.Add(new Paragraph($"UUID: {envelope.Uuid}"));
document.Add(new Paragraph($"Title: {envelope.Title}"));
document.Add(new Paragraph($"Subject: {envelope.Comment}"));
document.Add(new Paragraph($"Generated: {DateTime.UtcNow:O}"));
document.Add(new Paragraph(" "));
var table = new Table(4).UseAllAvailableWidth();
table.AddHeaderCell("Date");
table.AddHeaderCell("Status");
table.AddHeaderCell("User");
table.AddHeaderCell("EnvelopeId");
foreach (var item in reportItems.OrderByDescending(r => r.ItemDate))
{
table.AddCell(item.ItemDate.ToString("u"));
table.AddCell(item.ItemStatus.ToString());
table.AddCell(item.ItemUserReference);
table.AddCell(item.EnvelopeId.ToString());
}
document.Add(table);
document.Close();
return stream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not create report for envelope {EnvelopeId}", envelope.Id);
throw new CreateReportException("Could not prepare report data", ex);
}
}
private List<ReportItem> LoadReportItems(SqlConnection connection, int envelopeId)
{
const string sql = "SELECT ENVELOPE_ID, POS_WHEN, POS_STATUS, POS_WHO FROM VWSIG_ENVELOPE_REPORT WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<ReportItem>();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new ReportItem
{
EnvelopeId = reader.GetInt32(0),
ItemDate = reader.IsDBNull(1) ? DateTime.MinValue : reader.GetDateTime(1),
ItemStatus = reader.IsDBNull(2) ? default : (EnvelopeGenerator.Domain.Constants.EnvelopeStatus)reader.GetInt32(2),
ItemUserReference = reader.IsDBNull(3) ? string.Empty : reader.GetString(3)
});
}
return result;
}
}

View File

@@ -0,0 +1,19 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportItem
{
public Envelope? Envelope { get; set; }
public int EnvelopeId { get; set; }
public string EnvelopeTitle { get; set; } = string.Empty;
public string EnvelopeSubject { get; set; } = string.Empty;
public EnvelopeStatus ItemStatus { get; set; }
public string ItemStatusTranslated => ItemStatus.ToString();
public string ItemUserReference { get; set; } = string.Empty;
public DateTime ItemDate { get; set; }
}

View File

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

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -1,96 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -1,30 +0,0 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -1,105 +0,0 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -1,19 +0,0 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -1,36 +0,0 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

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

View File

@@ -1,64 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

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

View File

@@ -1,10 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUI
@using EnvelopeGenerator.ReceiverUI.Components

View File

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

View File

@@ -1,27 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -1,38 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26087",
"sslPort": 44331
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7124;http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

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

View File

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

View File

@@ -1,51 +0,0 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -5,6 +5,7 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Tests.Application;
@@ -44,9 +45,15 @@ public class DocSignedNotificationTests : TestBase
// Create envelope receiver
var envRcv = this.CreateEnvelopeReceiver(env!.Id, rcv.Id);
envRcv = await Repository.CreateAsync(envRcv, cancel);
var repo = GetRepository<EnvelopeReceiver>();
envRcv = await repo.CreateAsync(envRcv, cancel);
var envRcvDto = _mapper.Map<EnvelopeReceiverDto>(envRcv);
var docSignedNtf = envRcvDto.ToDocSignedNotification(new () { });
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();

View File

@@ -1,8 +1,10 @@
using Bogus;
using CommandDotNet;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Histories.Commands;
@@ -11,6 +13,7 @@ using EnvelopeGenerator.Application.Users.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Tests.Application;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -20,7 +23,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Tests.Application;
@@ -42,10 +44,29 @@ public class Fake
// add Application and Infrastructure services
#pragma warning disable CS0618
services.AddEnvelopeGeneratorServices(configuration);
services.AddEnvelopeGeneratorInfrastructureServices(
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
context.Configuration
);
var cnnStrName = "Default";
var connStr = context.Configuration.GetConnectionString(cnnStrName)
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
{
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
opt.AddDbTriggerParams(context.Configuration);
opt.AddDbContext((provider, options) =>
{
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
options.UseSqlServer(connStr)
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
});
var prodCnnStr = context.Configuration.GetConnectionString("Default");
services.AddDbContext<EGDbContext2Prod>(opt => opt.UseSqlServer(prodCnnStr));

View File

@@ -24,6 +24,8 @@ public abstract class TestBase : Faker
protected IRepository Repository => Host.Services.GetRequiredService<IRepository>();
protected IServiceProvider Services => Host.Services;
protected abstract void ConfigureServices(IServiceCollection services);
[SetUp]
@@ -32,9 +34,11 @@ public abstract class TestBase : Faker
Host = Fake.CreateHost(ConfigureServices);
await Host.AddSamples();
var repo = GetRepository<EmailTemplate>();
// Add seed email templates
foreach (var temp in SeedEmailTemplates)
await Repository.CreateAsync(temp);
await repo.CreateAsync(temp);
}
[TearDown]

View File

@@ -0,0 +1,17 @@
using EnvelopeGenerator.Domain.Constants;
using NUnit.Framework;
namespace EnvelopeGenerator.Tests.Domain;
public class ConstantsTests
{
[TestCase(EnvelopeSigningType.ReadAndSign, EnvelopeSigningType.ReadAndSign)]
[TestCase(EnvelopeSigningType.WetSignature, EnvelopeSigningType.WetSignature)]
[TestCase((EnvelopeSigningType)5, EnvelopeSigningType.WetSignature)]
public void Normalize_ReturnsExpectedValue(EnvelopeSigningType input, EnvelopeSigningType expected)
{
var normalized = input.Normalize();
Assert.That(normalized, Is.EqualTo(expected));
}
}

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.4.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />

View File

@@ -1,52 +0,0 @@
{
"homepage": "https://slavik0329.github.io/pdf-sign",
"name": "pdf-sign",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"annotpdf": "^1.0.12",
"dayjs": "^1.9.6",
"pdf-lib": "^1.12.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.3",
"react-dropzone": "^11.2.4",
"react-icons": "^4.1.0",
"react-pdf": "^5.0.0",
"react-scripts": "4.0.1",
"react-signature-canvas": "^1.0.3",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.1.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Finally, a simple way to put your signature on a PDF for free."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PDF-sign</title>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

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

View File

@@ -1,278 +0,0 @@
import "./App.css";
import { useRef, useState } from "react";
import Drop from "./Drop";
import { Document, Page, pdfjs } from "react-pdf";
import { PDFDocument, rgb } from "pdf-lib";
import { blobToURL } from "./utils/Utils";
import PagingControl from "./components/PagingControl";
import { AddSigDialog } from "./components/AddSigDialog";
import { Header } from "./Header";
import { BigButton } from "./components/BigButton";
import DraggableSignature from "./components/DraggableSignature";
import DraggableText from "./components/DraggableText";
import dayjs from "dayjs";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
function downloadURI(uri, name) {
var link = document.createElement("a");
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function App() {
const styles = {
container: {
maxWidth: 900,
margin: "0 auto",
},
sigBlock: {
display: "inline-block",
border: "1px solid #000",
},
documentBlock: {
maxWidth: 800,
margin: "20px auto",
marginTop: 8,
border: "1px solid #999",
},
controls: {
maxWidth: 800,
margin: "0 auto",
marginTop: 8,
},
};
const [pdf, setPdf] = useState(null);
const [autoDate, setAutoDate] = useState(true);
const [signatureURL, setSignatureURL] = useState(null);
const [position, setPosition] = useState(null);
const [signatureDialogVisible, setSignatureDialogVisible] = useState(false);
const [textInputVisible, setTextInputVisible] = useState(false);
const [pageNum, setPageNum] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [pageDetails, setPageDetails] = useState(null);
const documentRef = useRef(null);
return (
<div>
<Header />
<div style={styles.container}>
{signatureDialogVisible ? (
<AddSigDialog
autoDate={autoDate}
setAutoDate={setAutoDate}
onClose={() => setSignatureDialogVisible(false)}
onConfirm={(url) => {
setSignatureURL(url);
setSignatureDialogVisible(false);
}}
/>
) : null}
{!pdf ? (
<Drop
onLoaded={async (files) => {
const URL = await blobToURL(files[0]);
setPdf(URL);
}}
/>
) : null}
{pdf ? (
<div>
<div style={styles.controls}>
{!signatureURL ? (
<BigButton
marginRight={8}
title={"Add signature"}
onClick={() => setSignatureDialogVisible(true)}
/>
) : null}
<BigButton
marginRight={8}
title={"Add Date"}
onClick={() => setTextInputVisible("date")}
/>
<BigButton
marginRight={8}
title={"Add Text"}
onClick={() => setTextInputVisible(true)}
/>
<BigButton
marginRight={8}
title={"Reset"}
onClick={() => {
setTextInputVisible(false);
setSignatureDialogVisible(false);
setSignatureURL(null);
setPdf(null);
setTotalPages(0);
setPageNum(0);
setPageDetails(null);
}}
/>
{pdf ? (
<BigButton
marginRight={8}
inverted={true}
title={"Download"}
onClick={() => {
downloadURI(pdf, "file.pdf");
}}
/>
) : null}
</div>
<div ref={documentRef} style={styles.documentBlock}>
{textInputVisible ? (
<DraggableText
initialText={
textInputVisible === "date"
? dayjs().format("M/d/YYYY")
: null
}
onCancel={() => setTextInputVisible(false)}
onEnd={setPosition}
onSet={async (text) => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y +
(12 * scale) -
position.offsetY -
documentRef.current.offsetTop);
const x =
position.x -
166 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
firstPage.drawText(text, {
x: newX,
y: newY,
size: 20 * scale,
});
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setTextInputVisible(false);
}}
/>
) : null}
{signatureURL ? (
<DraggableSignature
url={signatureURL}
onCancel={() => {
setSignatureURL(null);
}}
onSet={async () => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y -
position.offsetY +
64 -
documentRef.current.offsetTop);
const x =
position.x -
160 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
const pngImage = await pdfDoc.embedPng(signatureURL);
const pngDims = pngImage.scale( scale * .3);
firstPage.drawImage(pngImage, {
x: newX,
y: newY,
width: pngDims.width,
height: pngDims.height,
});
if (autoDate) {
firstPage.drawText(
`Signed ${dayjs().format(
"M/d/YYYY HH:mm:ss ZZ"
)}`,
{
x: newX,
y: newY - 10,
size: 14 * scale,
color: rgb(0.074, 0.545, 0.262),
}
);
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setSignatureURL(null);
}}
onEnd={setPosition}
/>
) : null}
<Document
file={pdf}
onLoadSuccess={(data) => {
setTotalPages(data.numPages);
}}
>
<Page
pageNumber={pageNum + 1}
width={800}
height={1200}
onLoadSuccess={(data) => {
setPageDetails(data);
}}
/>
</Document>
</div>
<PagingControl
pageNum={pageNum}
setPageNum={setPageNum}
totalPages={totalPages}
/>
</div>
) : null}
</div>
</div>
);
}
export default App;

View File

@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,37 +0,0 @@
import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { cleanBorder, primary45 } from "./utils/colors";
export default function Drop({ onLoaded }) {
const styles = {
container: {
textAlign: "center",
border: cleanBorder,
padding: 20,
marginTop: 12,
color: primary45,
fontSize: 18,
fontWeight: 600,
borderRadius: 4,
userSelect: "none",
outline: 0,
cursor: "pointer",
},
};
const onDrop = useCallback((acceptedFiles) => {
onLoaded(acceptedFiles);
// Do something with the files
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: "application/pdf",
});
return (
<div {...getRootProps()} style={styles.container}>
<input {...getInputProps()} />
{isDragActive ? <p>Drop a PDF here</p> : <p>Drag a PDF here</p>}
</div>
);
}

View File

@@ -1,15 +0,0 @@
import {primary45} from "./utils/colors";
export function Header() {
const styles = {
container: {
backgroundColor: primary45,
color: '#FFF',
padding: 12,
fontWeight: 600,
}
}
return <div style={styles.container}>
<div>Open PDF Sign</div>
</div>
}

View File

@@ -1,77 +0,0 @@
import { Dialog } from "./Dialog";
import SignatureCanvas from "react-signature-canvas";
import { ConfirmOrCancel } from "./ConfirmOrCancel";
import { primary45 } from "../utils/colors";
import { useRef } from "react";
export function AddSigDialog({ onConfirm, onClose, autoDate, setAutoDate }) {
const sigRef = useRef(null);
const styles = {
sigContainer: {
display: "flex",
justifyContent: "center",
},
sigBlock: {
display: "inline-block",
border: `1px solid ${primary45}`,
},
instructions: {
display: "flex",
justifyContent: "space-between",
textAlign: "center",
color: primary45,
marginTop: 8,
width: 600,
alignSelf: "center",
},
instructionsContainer: {
display: "flex",
justifyContent: "center",
},
};
return (
<Dialog
isVisible={true}
title={"Add signature"}
body={
<div style={styles.container}>
<div style={styles.sigContainer}>
<div style={styles.sigBlock}>
<SignatureCanvas
velocityFilterWeight={1}
ref={sigRef}
canvasProps={{
width: "600",
height: 200,
className: "sigCanvas",
}}
/>
</div>
</div>
<div style={styles.instructionsContainer}>
<div style={styles.instructions}>
<div>
Auto date/time{" "}
<input
type={"checkbox"}
checked={autoDate}
onChange={(e) => setAutoDate(e.target.checked)}
/>
</div>
<div>Draw your signature above</div>
</div>
</div>
<ConfirmOrCancel
onCancel={onClose}
onConfirm={() => {
const sigURL = sigRef.current.toDataURL();
onConfirm(sigURL);
}}
/>
</div>
}
/>
);
}

View File

@@ -1,81 +0,0 @@
import React from "react";
import { primary45 } from "../utils/colors";
import useHover from "../hooks/useHover";
export function BigButton({
title,
onClick,
inverted,
fullWidth,
customFillColor,
customWhiteColor,
style,
noHover,
id,
small,
disabled,
marginRight,
}) {
const [hoverRef, isHovered] = useHover();
let fillColor = customFillColor || primary45;
const whiteColor = customWhiteColor || "#FFF";
let initialBg = null;
let hoverBg = fillColor;
let initialColor = fillColor;
let hoverColor = whiteColor;
if (inverted) {
initialBg = fillColor;
hoverBg = null;
initialColor = whiteColor;
hoverColor = fillColor;
}
if (disabled) {
initialBg = "#ddd";
hoverBg = "#ddd";
fillColor = "#ddd";
}
const styles = {
container: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: fullWidth ? "100%" : null,
backgroundColor: isHovered && !noHover ? hoverBg : initialBg,
color:
isHovered && !noHover && !disabled
? hoverColor
: disabled
? "#999"
: initialColor,
borderRadius: 4,
padding: small ? "2px 4px" : "6px 8px",
fontSize: small ? 14 : null,
border: `1px solid ${fillColor}`,
cursor: !disabled ? "pointer" : null,
userSelect: "none",
boxSizing: "border-box",
marginRight,
},
};
return (
<div
id={id}
ref={hoverRef}
style={{ ...styles.container, ...style }}
onClick={() => {
if (!disabled) {
onClick();
}
}}
>
{title}
</div>
);
}

View File

@@ -1,37 +0,0 @@
import { BigButton } from "./BigButton";
import React from "react";
export function ConfirmOrCancel({
onCancel,
onConfirm,
confirmTitle = "Confirm",
leftBlock,
hideCancel,
disabled
}) {
const styles = {
actions: {
display: "flex",
justifyContent: "space-between",
},
cancel: {
marginRight: 8,
},
};
return (
<div style={styles.actions}>
<div>{leftBlock}</div>
<div>
{!hideCancel ? (
<BigButton
title={"Cancel"}
style={styles.cancel}
onClick={onCancel}
/>
) : null}
<BigButton title={confirmTitle} inverted={true} onClick={onConfirm} disabled={disabled}/>
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More