Compare commits

..

198 Commits

Author SHA1 Message Date
0cf603fc79 Swap steps 1 and 3 in CreateEnvelopeWizardPopup
Reorganize the wizard steps to prioritize "Receivers" (step 1)
before "Message and Options" (step 3). Update `_currentStep`
logic, conditional checks, and validation to reflect the new
step order. Adjust `GetStepTitle` and `GetStepDescription`
methods to align with the updated step titles and descriptions.
Ensure user input handling and step-skipping behavior are
consistent with the new flow.
2026-11-05 16:18:17 +01:00
e3d2225a44 Refactor and enhance envelope wizard with MudBlazor
Replaced custom wizard UI with MudBlazor's MudStepper for a modern and consistent design. Added dynamic step titles, descriptions, and forms for envelope creation, including advanced settings like certification, reminders, and email notifications. Integrated PDF upload with preview functionality.

Refactored `CreateEnvelopeWizardPopup.razor` for better maintainability and modularity. Removed redundant code and centralized step-related logic. Improved localization, accessibility, and user experience.

Updated `EnvelopeGenerator.Server.Client.csproj` and `EnvelopeGenerator.Server.csproj` to include MudBlazor. Registered MudBlazor services in `Program.cs` and added required CSS/JS in `App.razor`. Updated styles in `sender-page.css` to align with the new stepper design.
2026-11-05 15:22:21 +01:00
595bbf3f25 chore(db): back up email templates before redesign
Store the original template subjects and bodies as restore scripts so the redesigned notifications can be reverted against DD_ECM if needed.
2026-11-05 12:03:44 +01:00
ee7d01ed7f test(notification): add email notification preview endpoints
Expose signed and received notification endpoints, with reset random payload variants, to exercise rendering and delivery without running a full envelope workflow.

Fall back to a fixed recipient when no email is supplied.
2026-11-05 12:03:40 +01:00
85104511fd feat(notification): distinguish read confirmation from signature
Add IsReadAndConfirm to DocumentSignedNotification and derive the action, status, and final action labels from it so read-only envelopes do not report a signature.

Pass the envelope read-and-confirm mode when finalizing so the signed email matches the receiver workflow.
2026-11-05 12:03:35 +01:00
1b87528cca refactor(notification): publish emails through messaging client
Send notifications via IMessagingServiceClient and PublishEmailViaSmtpCommand instead of the static EmailSender. The sender address now comes from NotificationOptions and attachments are added through the command builder.

Rename the Notify parameter to receiverEmail so the call site contract is explicit.
2026-11-05 12:03:31 +01:00
2913514da8 feat(notification): make messaging consumer configurable
Replace the hard-coded RabbitMQ setup with NotificationOptions bound from a configurable section. The new ConsumerType switch wires either the HTTP API client or the RabbitMQ client, and startup now fails with a clear error when required settings are missing.

Bump DigitalData.MessagingService.Client to 3.0.1 for the abstraction used by both transports.
2026-11-05 12:03:26 +01:00
ce7e70162b Remove ReplacePlaceHolders method
The private static method `ReplacePlaceHolders` was removed from `CreateEnvelopeCommandHandler.cs`. This method was used to replace placeholders in a string with values from a dictionary. Its removal indicates that this functionality is either no longer needed or has been refactored elsewhere in the codebase.
2026-10-29 15:57:29 +01:00
5e00b6e9ec Refactor notification and email handling
Refactored `DocumentReceivedNotification` to use new properties `Origin`, `EnvelopeUuid`, and `ReceiverSignature` for constructing `EnvelopeURL`. Updated `LinkToDocument` and `LinkToDocumentText` to derive from `EnvelopeURL`.

Simplified `CreateEnvelopeCommandHandler` with record-like syntax, removing private fields and directly injecting dependencies.

Renamed and refactored `QueueInvitationEmailsAsync` to `SendInvitationEmailsAsync`, using `EnvNtfService.Notify` for email notifications, centralizing logic and improving clarity.

Improved error handling in email sending with logging. Updated variable names for clarity and cleaned up `using` directives.
2026-10-29 15:54:07 +01:00
f0b4c8d00c Refactor ContractType property to use enum
Updated the `ContractType` property in the `EnvelopeTypeDto` class from a nullable integer (`int?`) to a nullable `ContractType` enum (`ContractType?`). This change enhances type safety and clarifies the expected values for the property.
2026-10-29 15:52:38 +01:00
abf616c115 Add IRequestContext interface and RequestContext class
Introduce the IRequestContext interface with an Origin property
in the EnvelopeGenerator.Application.Common.Interfaces namespace.
Implement this interface in the new RequestContext class within
EnvelopeGenerator.Server.Services, using IHttpContextAccessor to
access the Origin header. Register IRequestContext with the DI
container in Program.cs using AddScoped. Update using directives
to include necessary namespaces and reorganize for clarity.
2026-10-29 15:51:58 +01:00
42585e1f2c Make User property conditionally nullable
Modified the User property in Envelope.cs to be conditionally nullable using the nullable preprocessor directive. This change allows the property to be nullable when the nullable directive is defined, enhancing flexibility and compatibility with different C# versions and project configurations.
2026-10-29 15:51:19 +01:00
ff10fc6b0a Refactor ContractType property in EnvelopeType.cs
Added a using directive for `EnvelopeGenerator.Domain.Constants` to utilize constants from that namespace. Changed the `ContractType` property from a nullable integer (`int?`) to a nullable `ContractType` enum or class, enhancing type safety and clarity.
2026-10-29 13:35:48 +01:00
7222f7e9a7 Refactor ContractType to use enum
Changed the `ContractType` property in `EnvelopeDto`, `CreateEnvelopeCommandHandler`, and `Envelope` classes from a nullable `int` to a nullable `ContractType` enum. This improves type safety and consistency across the codebase by using a specific enum type instead of a generic integer. Updated logic in `CreateEnvelopeCommandHandler` to remove unnecessary casting.
2026-10-29 13:11:44 +01:00
d913b949e0 Add DTOs and tests for placeholder replacement
Added new record types `CreateEnvelopeDocumentDto`, `CreateEnvelopeFieldDto`, and `CreateEnvelopeReceiverDto` with XML documentation in `CreateEnvelopeCommand.cs`. Marked `ConfigService` as obsolete in `CreateEnvelopeCommandHandler.cs`. Modified `CertificationType` usage in `Fake.cs` to use the enum directly. Updated `EnvelopeGenerator.Tests.csproj` with a new project reference and compile directives. Introduced `MailTemplatePlaceholderDbTests` and `ReplacePlaceholderExtensionsTests` to verify placeholder replacement functionality. Added supporting model classes for testing. These changes improve code functionality and test coverage.
2026-10-29 09:53:52 +01:00
a35e05d016 Refactor Attachments property and syntax
Changed the `Attachments` property type in `EnvelopeNotificationBase.cs` from `List` to `IReadOnlyList` to enhance immutability and thread safety. Updated the `FinalizeEnvelopeCommandHandler` in `FinalizeEnvelopeCommand.cs` to use a single-line syntax for `Attachments` initialization, improving code readability.
2026-10-28 16:28:17 +01:00
8aabc3e233 Refactor notification system
Introduced `EnvelopeNotificationBase` as a new base class for notifications, providing a unified structure with an abstract `GetTemplateType` method and optional attachments. Added `DocumentReceivedNotification` and updated `DocumentSignedNotification` to inherit from this base class, streamlining attachment handling and template type determination.

Refactored `IEnvelopeNotificationService` to use a generic `Notify` method, allowing for flexibility with different notification types. Updated `FinalizeEnvelopeCommandHandler` to align with the new attachment handling.

Added `EnvelopeNotificationExtensions` for converting attachments to `EmailAttachmentDto` objects and introduced `ReplacePlaceholderExtensions` for dynamic placeholder replacement in email templates using reflection. Removed outdated placeholder logic from `EnvelopeNotificationService`.
2026-10-28 16:25:19 +01:00
8b7f99eb76 Prevent email on deleted envelopes; bump version
Added a check in SendMailHandler to prevent sending emails for deleted envelopes with a status of EnvelopeSaved or lower. Updated project version in EnvelopeGenerator.Server.csproj from 1.7.0 to 1.8.0 to reflect these changes.
2026-10-28 13:15:20 +01:00
91e90b322b Add caching to PdfRenderController
Implemented caching in the PdfRenderController using IMemoryCache to improve performance by avoiding redundant PDF rendering. Added a new constructor parameter for IMemoryCache and a static TimeSpan field for cache duration. Introduced a private method, BuildCacheKey, to generate unique cache keys based on the SHA256 hash of PDF bytes. Updated the RenderPages method to check for cached pages before rendering and store results in the cache.
2026-10-27 21:57:35 +01:00
74e55e9206 Refactor and optimize envelope handling
Refactored `ReadDocumentDetailsQueryHandler` to remove AutoMapper, improving performance by manually selecting necessary data. Simplified `SenderEditorPage.razor` by removing bootstrap logic and updating the UI for a more compact design. Streamlined envelope creation in `SenderPage.razor` by eliminating the bootstrap token. Updated styles in `envelope-viewer.css` for better alignment and readability.
2026-10-27 21:56:39 +01:00
fc5ad7ea3c Add receiver removal functionality to SenderEditor
Enhanced the `SenderEditorPage.razor` by injecting the `EnvelopeReceiverService` to manage envelope receivers. Updated the UI to include a remove button for each receiver, invoking the new `RemoveReceiverAsync` method. This method handles the deletion of receivers from both the UI and the server.

Modified `ReceiverDraft` and `SignatureFieldDraft` models to include `ReceiverId` and `ReceiverDraftId` for better tracking. Updated logic for identifying receivers without signature fields and creating receiver commands to use these new identifiers.

Added `DeleteAsync` method in `EnvelopeReceiverService.cs` to send DELETE requests for removing receivers. Updated `envelope-viewer.css` to style the new remove button with distinct appearance and hover effects.
2026-10-27 19:02:22 +01:00
ff5582c9b5 Add RemoveEnvelopeReceiverCommand and refactor controller
Introduced `RemoveEnvelopeReceiverCommand` and its handler to
facilitate the removal of envelope receivers. Updated the
`EnvelopeReceiverController` to use C# 9's record-like syntax
for dependency injection, eliminating explicit private fields.
Added a `RemoveAsync` method to handle DELETE requests for
removing envelope receivers. Enhanced code documentation with
additional XML comments.
2026-10-27 18:32:47 +01:00
6523cca945 Refactor certification type option values
Updated the `value` attributes of `<option>` elements in `CreateEnvelopeWizardPopup.razor` and `SenderEditorPage.razor` to use `@CertificationType.AdvancedElectronicSignature` and `@CertificationType.ElectronicSignature` instead of hardcoded values "1" and "2". This enhances code maintainability and readability by leveraging descriptive and potentially type-safe values.
2026-10-27 16:11:55 +01:00
28d0b6f81c Add feature to open receiver envelope URLs
Introduce a new feature allowing users to open URLs for receiver envelopes directly from the sender page. This includes:

- Injecting `EnvelopeReceiverService` in `SenderPage.razor`.
- Adding a UI button to open receiver envelope URLs.
- Implementing `OpenReceiverEnvelopeUrlAsync` for URL handling.
- Extending `EnvelopeReceiverService` with methods for URL retrieval and feature check.
- Updating `EnvelopeReceiverController` with new endpoints for URL access.
- Introducing `SenderFeatureAccessOptions` for managing feature access.
- Configuring feature access in `Program.cs` and `appsettings.json`.
- Restricting feature usage to specified email addresses.
2026-10-27 11:37:25 +01:00
5df38754cb Add configurable SignatureFieldParams class
Introduce `SignatureFieldParams` class to manage signature field dimensions with configurable `Width` and `Height`. Integrate this configuration into the service collection and replace hardcoded dimensions in `CreateEnvelopeCommandHandler.cs` and `SenderEditorPage.razor`. Update `appsettings.json` to include the new configuration section, allowing runtime adjustments.
2026-10-27 09:18:11 +01:00
c2b7579717 Refactor CertificationType to use enum
Changed CertificationType from int? to CertificationType? enum across multiple files, including EnvelopeDto, EnvelopeTypeDto, and CreateEnvelopeCommand, to improve type safety. Removed the NormalizeCertificationType method as it is no longer necessary. Updated components and records to use the enum directly, enhancing code clarity and reducing potential errors. Added necessary using directives for the enum.
2026-10-26 16:59:49 +01:00
ae1ecbc15f Normalize certification type handling
Introduce `NormalizeCertificationType` method to ensure consistent handling of certification types across the application. Update `CreateEnvelopeCommandHandler`, `CreateEnvelopeWizardPopup.razor`, and `SenderEditorPage.razor` to use this method. Modify `CertificationType` enum to include `ElectronicSignature` and remove `QualifiedSignature`. Update UI components to reflect these changes by removing the `QualifiedSignature` option.
2026-10-26 16:58:06 +01:00
4dcfe67038 Increase grid columns in sender wizard progress
Updated the CSS grid layout for the `.sender-create-wizard__progress` class from 4 to 5 columns. This change allows for an additional column in the progress indicator, potentially accommodating more items or steps in the sender creation wizard.
2026-10-26 15:55:57 +01:00
c812d4dc1f Refactor EnvelopeController for DI and clarity
Refactored the `EnvelopeController` to use C# 9's record syntax for dependency injection, directly injecting `ILogger<EnvelopeController>` and `IMediator` into the constructor. Removed private fields `_logger` and `_mediator`, replacing them with constructor-injected properties. Updated methods to use these properties for consistency. Moved `DeleteEnvelopeRequest` to a top-level class for better organization. Enhanced XML documentation for improved clarity.
2026-10-26 15:49:33 +01:00
8d27718062 Add IncludeDocAndDocRes option to envelope queries
Introduced a new boolean property `IncludeDocAndDocRes` in the `ReadEnvelopeQuery` class to control the inclusion of document and document result details in query results. Updated `ReadEnvelopeQueryHandler` to handle this property, returning a simplified `EnvelopeDto` when set to `false`.

Modified `SenderPage.razor` to exclude document details by passing `includeDocAndDocRes: false` to `EnvelopeService.GetAsync()`. Enhanced `EnvelopeService` to accept and process the `includeDocAndDocRes` parameter, ensuring API requests respect this preference.
2026-10-26 15:24:18 +01:00
c746612d80 Merge branch 'feat/server-hsm-integr' 2026-10-26 14:45:59 +01:00
06551d502b Update branding to "signFLOW" for consistency
The footer text in `MainLayout.razor` and the header title in `IndexPage.razor` have been updated to change the branding from "SignFlow" to "signFLOW". This change ensures consistent presentation of the brand across the application by adjusting the capitalization.
2026-10-26 09:47:36 +01:00
0f28b80e16 Update notification logic and version numbers
Revised FinalizeEnvelopeCommand to notify each receiver
individually instead of converting documents to Base64.
Adjusted IsActive method in EnvelopeStatusExtensions to
include EnvelopePartlySigned as active. Updated project
version to 1.7.0-beta to reflect these changes.
2026-10-25 18:52:21 +01:00
f7af3776c2 Switch to DocumentService API, remove PKCS#11 support
Replaced PKCS#11-based cryptographic operations with a new DocumentService API integration. Removed all PKCS#11-related code, configurations, and dependencies, including options, self-test status, and signing logic. Updated `FinalizeEnvelopeCommand` to include a notification service and added base64 conversion for documents. Updated `ServiceCollectionExtensions` to register the new DocumentService client. Removed outdated documentation and diagrams related to PKCS#11. Updated project files and configurations to reflect these changes and incremented the version number. Added a comment in `EnvelopeFinalizationWorker` for future RabbitMQ integration.
2026-10-25 18:52:12 +01:00
8ba9e8ec20 Update package versions and DI service registration
Updated `EnvelopeGenerator.Infrastructure.Crypt.csproj` to add `DocumentService.Client` version `1.3.0` and upgraded several Microsoft.Extensions packages to version `10.0.10`.

In `Program.cs`, changed `DocumentService` registration to use the fully qualified name `EnvelopeGenerator.Server.Client.Services.DocumentService`.

Updated `EnvelopeGenerator.Tests.csproj` to upgrade `Microsoft.Extensions.DependencyInjection` and related packages to version `10.0.10`, including changes for the `net8.0` target framework.
2026-10-22 03:03:37 +02:00
226d852c6f Add envelope finalize endpoint 2026-10-22 01:35:26 +02:00
fb155fe706 Wire notification service and finalization worker into server host 2026-10-22 01:35:22 +02:00
658cb28e1c Add notification infrastructure project 2026-10-22 01:35:18 +02:00
f38098aed8 Add envelope finalization command and signed-envelope query 2026-10-22 01:35:14 +02:00
00c3fd18a9 Add envelope notification contracts 2026-10-22 01:35:10 +02:00
8929c517fe Map envelope DocResult and expose burned-state indicator 2026-10-22 01:35:06 +02:00
07ce6ccfe1 Use AuthorizationException for domain-specific errors
Replaced UnauthorizedAccessException with a custom
AuthorizationException in SignPdfByEnvelopeCommandHandler
to provide more meaningful error handling. Added the
EnvelopeGenerator.Domain.Exceptions namespace to support
this change.
2026-10-21 19:07:35 +02:00
26beae0d14 Add support for signed PDF previews
Integrated `PdfSignatureService` to enable fetching and previewing signed PDFs for completed envelopes. Updated `SenderPage.razor` to dynamically adjust popup headers and filenames based on the envelope's status. Enhanced `OpenPreviewAsync` to conditionally fetch signed or original PDFs.

Registered `PdfSignatureService` and DevExpress PDF Viewer in `Program.cs`. Implemented `PdfSignatureService` to handle API calls for retrieving signed PDFs with error handling.
2026-10-21 17:46:38 +02:00
e423c6d751 Allow receiver signing command to reuse an existing document stream 2026-10-21 01:28:16 +02:00
56d1dc050f Add sender endpoint for envelope-level PDF signing 2026-10-21 01:28:07 +02:00
016dec74f9 Add domain exception handling for authorization and data conflicts 2026-10-21 01:27:52 +02:00
b9cd629766 Add e-signature support and improve PDF signing process
Introduced `ESignParams` for configurable e-signature settings,
including `ApplicationName` and `DefaultReason`. Updated
`DocReceiverElementDto` and `PdfSignOptionsDto` to support
new properties. Registered `ESignParams` in DI container.

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

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

Added `DataIntegrityException` for better error handling.
Updated `appsettings.json` with `ESignParams` configuration.
2026-10-20 17:04:15 +02:00
e729adb441 Support flexible coordinate systems for PDF signatures
Added enums `PdfSignatureCoordinateUnit` and `PdfSignatureCoordinateOrigin` to support defining signature coordinates in points or inches and relative to either the bottom-left or top-left corner of the page.

Updated `PdfSignatureAppearanceDto` to include `CoordinateUnit` and `CoordinateOrigin` parameters with default values. Refactored `PdfDigitalSignatureService` to use a new `BuildSignatureBounds` method for calculating signature bounds, which handles unit conversion, origin adjustments, and validation.

Introduced `GetPageHeightInPoints` to retrieve page height for top-left origin calculations. Improved signature field naming precision and added error handling for invalid dimensions and page numbers.

Included a TODO for optimizing the signature check algorithm.
2026-10-20 14:18:20 +02:00
67d0b07926 Add support for signing PDFs with DocReceiverElement
Extended `DocReceiverElementDto` to include an optional `Document` reference. Enhanced `ReadDocReceiverElementQuery` with `Id` and `IncludeDocument` properties for filtering and document inclusion.

Updated `ReadDocReceiverElementQueryHandler` to handle new query properties, enabling filtering by `Id` and optionally including associated documents.

Introduced `SignPdfByElementCommand` and its handler to support signing PDFs using either a provided `DocReceiverElementDto` or an `ElementId`. Added validation via `SignPdfByElementCommandValidator` to ensure proper usage.

Refactored and added necessary `using` directives for new dependencies.
2026-10-20 10:55:41 +02:00
780277d656 Add DATABASE_STRUCTURE_CONTEXT.md for DB documentation
Added `DATABASE_STRUCTURE_CONTEXT.md` to provide detailed
documentation of the database structure for the envelope
domain. This includes entity-to-table mappings, schema
snapshots, EF relationships, constraints, stored procedure
behavior, and trigger wiring paths.

Highlighted model-to-database drift risks, trigger mismatch
alerts, and provided migration-safe operating rules. Added a
refresh checklist to validate metadata consistency before and
after database migrations.
2026-10-20 10:07:25 +02:00
635d5d8278 Refactor namespaces for CreateEnvelopeReceiver
Updated namespaces for `CreateEnvelopeReceiverCommand` and related classes to `EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver` for better organization and clarity.

- Updated namespaces in `CreateEnvelopeReceiverCommand`, `CreateEnvelopeReceiverCommandHandler`, and `CreateEnvelopeReceiverResponse`.
- Adjusted imports in `MappingProfile.cs`, `EnvelopeReceiverService.cs`, and `Fake.cs` to reflect the new namespace.
- Modified `EnvelopeReceiverController.cs` to use the updated namespace and simplified type reference in a `foreach` loop.
2026-10-19 15:50:32 +02:00
f1c8e223dd Refactor ComposeSignaturePreviewPng to return Stream
Updated the `ComposeSignaturePreviewPng` method in the `IPdfPageImageRenderer` interface to return a `Stream` instead of a `byte[]` for improved memory efficiency when handling large image data.

- Added `ComposeSignaturePreviewPngBytes` extension method to convert the `Stream` to a `byte[]` for backward compatibility.
- Updated `PdfPageImageRenderer` to return a `Stream` and adjusted its internal logic to reset the `MemoryStream` position before returning.
- Modified `DevSignatureInkController` to use the `Stream`-based implementation for HTTP responses.
- Made minor formatting adjustments in `PdfPageImageRenderer`.

These changes optimize memory usage while maintaining compatibility with existing workflows that require `byte[]`.
2026-10-19 14:49:48 +02:00
31bf64a6f2 Refactor TODOs for licensing and maintainability
Added a critical pre-commercial release TODO to remove
`SixLabors.ImageSharp` and `QRCoder-ImageSharp` dependencies
and replace them with permissive-license alternatives. Updated
TODOs to focus on modularizing `EnvelopeSenderPage.razor`
and other server pages by splitting large components,
centralizing shared logic, and improving maintainability.

Included a TODO for a senior-level performance review of
sender/receiver flows to address route transitions, loading
logic, and perceived latency issues. Removed outdated TODO
on reducing `EnvelopeSenderPage.razor` complexity.
2026-10-19 14:43:11 +02:00
93142a56f7 Add signature ink handling and DevExpress integration
Introduced `ISignatureInkPayload` interface to standardize signature ink data and metadata. Updated `DocReceiverElementDto` and `DocReceiverElement` to implement this interface. Enhanced `IPdfPageImageRenderer` with methods for signature preview generation and image format validation.

Replaced `PdfSharp` with `DevExpress` for PDF rendering in `ReceiverSignedPage.razor`. Added `DevSignatureInkController` with endpoints for downloading raw and composed signature images. Updated project files to remove `SixLabors.ImageSharp` and integrate DevExpress.

Added configuration for development endpoints in `appsettings.json`. Improved exception handling, validation, and code maintainability.
2026-10-19 14:27:21 +02:00
64baef907c Replace UUID with DocumentElementId across the codebase
Updated `PdfSignOptionsDto` and `PdfSignatureCheckItemDto` to use `DocumentElementId` (long) instead of `UUID` (string). Modified `PdfDigitalSignatureService` to reflect this change, including updates to `SignatureReasonMetaRegex`, `BuildReasonWithMeta`, and `ParseReasonMeta`. Adjusted method signatures, return values, and property assignments to ensure consistent use of `DocumentElementId`. These changes align with a shift in the application's data model to use numeric identifiers for document elements.
2026-10-19 11:22:12 +02:00
232b341894 Refactor PDF signing logic and appearance handling
Refactored `Pkcs11Options` by removing `DefaultCertificationLevel` and improving property documentation. Replaced `VisualSignature` in `PdfSignOptionsDto` with `Appearance` using the new `PdfSignatureAppearanceDto` class, which encapsulates signature appearance details, including font and color settings.

Enhanced `PdfSignatureCertificationLevel` enum with more descriptive values and added XML documentation. Updated `IPdfDigitalSignatureService` to support optional signature images and removed redundant methods.

Refactored `PdfDigitalSignatureService` to streamline appearance handling, validate inputs, and improve maintainability. Removed unused methods and added support for `PdfSignatureAppearanceDto`.

Simplified `PdfSignatureController` by consolidating form properties into `PdfSignOptionsDto`. Improved code readability and flexibility while removing redundant logic.
2026-10-19 10:48:06 +02:00
57a08e8a89 Refactor DTOs and simplify PdfDigitalSignatureService
Refactored `PdfSignOptionsDto` and `PdfVisualSignatureOptionsDto`:
- Converted both to immutable `record` types with non-nullable properties.
- Added XML documentation comments for properties and parameters.

Simplified `PdfDigitalSignatureService`:
- Updated `SignPdfAsync` to enforce non-nullable `PdfSignOptionsDto`.
- Removed redundant null checks and inlined `CreateSignatureBounds`.
- Simplified signature appearance and image data logic.
- Refactored `TryGetKeySize` to use `bool` with `out` parameter.

General improvements:
- Enhanced code readability and maintainability.
- Reduced nullable references and enforced stricter type safety.
2026-10-16 10:43:11 +02:00
198eabcbae Add support for visual PDF signatures
Enhanced `PdfSignOptionsDto` with a `VisualSignature` property and introduced the `PdfVisualSignatureOptionsDto` class for configuring visual signature options. Updated `PdfDigitalSignatureService` to handle visual signatures, including page number resolution, signature bounds calculation, and appearance customization.

Refactored `PdfSignatureController` to support visual signatures by extending `SignPdfForm` with `SignatureImageFile` and `VSignature` properties. Improved validation for page numbers and signature dimensions. Streamlined `Sign` and `IsSigned` endpoints for better clarity and reduced redundancy.

Added XML documentation for improved API clarity and performed general code cleanup to enhance maintainability.
2026-10-16 10:10:04 +02:00
04cea1ca4b Refactor to use streams for PDF handling
Refactored `IPdfDigitalSignatureService` to replace `byte[]` with `Stream` for input and output, improving memory efficiency and scalability. Updated `PdfDigitalSignatureService` implementation to support stream-based operations, including signing, checking, and extracting PDFs.

Modified `SignPdfAsync` to handle multiple signatures sequentially using streams and ensured proper disposal of intermediate streams. Updated `CheckPdfSignatureAsync` and `ExtractOriginalPdfAsync` to use streams, reducing memory overhead.

Refactored `PdfSignatureController` to use streams for file handling, enabling efficient processing of large files. Introduced helper methods for stream handling and enhanced cancellation support. Improved code maintainability by consolidating common logic and reducing duplication.
2026-10-15 11:04:05 +02:00
29108594d9 Update project version to 1.5.0-beta
Updated the `EnvelopeGenerator.Server` project version from `1.4.0-beta` to `1.5.0-beta`.
This includes updates to the file version (from `1.4.0.0` to `1.5.0.0`)
and the assembly version (from `1.4.0.0` to `1.5.0.0`).
2026-10-14 15:11:19 +02:00
51c0f51738 Refactor contact info handling in PDF signing
Replaced the `ContactInfo` property with separate `Email` and `Phone` fields in `PdfSignOptionsDto` and `PdfSignatureCheckItemDto` for better granularity. Updated the `PdfDigitalSignatureService` to handle the new fields, introducing `BuildContactInfo` and `ParseContactInfo` methods for constructing and parsing contact information.

Modified the `PdfSignatureController` to reflect these changes, updating request models and the `SignPdfAsync` method. Updated the `README.md` to document the new structure. These changes improve the clarity and structure of contact information handling.
2026-10-14 15:10:55 +02:00
816bbce9c6 Remove client-side SigningTime handling
The `SigningTime` property has been removed from the `PdfSignOptionsDto` class and related client-side input handling. It is now set automatically on the server side based on the `UseUtcSigningTime` configuration (`UtcNow` or local time).

Updated the `PdfDigitalSignatureService` to always set `SigningTime` server-side, ensuring consistent and deterministic behavior. Removed `SigningTime` from the `PdfSignatureController` and adjusted the logic to exclude it from the `signOptions` object.

Updated the `README.md` to reflect these changes, clarifying that `SigningTime` is now server-side only. These changes improve security and simplify client integration.
2026-10-14 14:49:15 +02:00
5194d638ac Refactor README.md with localized and detailed guidance
The README.md has been extensively restructured to improve clarity, technical depth, and usability. Key changes include:

- Shifted most sections from English to German for localization.
- Consolidated distributed runbooks into a unified reference for PKCS#11/SoftHSM integration.
- Expanded architecture overview with deployment patterns and runtime paths.
- Replaced PKCS#11 basics with detailed setup instructions and troubleshooting tips.
- Updated runtime configuration examples with security best practices.
- Highlighted implemented services, models, and DTOs in the DI setup.
- Added sections on operational challenges, troubleshooting, and monitoring.
- Introduced an API health endpoint and smoke test recommendations.
- Clarified API behavior for signature metadata to ensure deterministic integration.
- Expanded security guidelines, including key rotation and SSH rollback procedures.
- Added a forward-looking section on potential improvements.
- Retained and updated references to official documentation and tools.

These changes aim to provide a comprehensive, structured, and production-aligned guide for developers and engineers.
2026-10-14 14:47:40 +02:00
6b3622b366 Remove test endpoint from PdfSignatureController
Removed the `[HttpGet("sign/test")]` endpoint and its `Test`
method from the `PdfSignatureController`. This method
contained hardcoded Base64 PDF data and predefined signing
options for testing purposes. Eliminated the associated
logic for decoding the Base64 string, creating signing
options, signing the PDF, and returning the signed file.

These changes streamline the controller by removing
unnecessary and potentially insecure functionality,
ensuring cleaner and more production-ready code.
2026-10-14 14:35:58 +02:00
4f8fa9afd8 Rename DefaultApplicationName to ApplicationName
Updated the `Pkcs11Options` class to rename the `DefaultApplicationName` property to `ApplicationName`, clarifying its role as a globally controlled value embedded in PDF signature metadata.

Modified `PdfDigitalSignatureService` to source `ApplicationName` directly from `Pkcs11Options`, removing client-level overrides. Updated `PdfSignatureController` to reflect this change by removing the `ApplicationName` property from `SignOptions`.

Updated `appsettings.json` and `README.md` to reflect the property name change. These changes enforce stricter configuration control, improving metadata consistency and security.
2026-10-14 14:34:59 +02:00
7a36316b7e Standardize CertificationLevel in PDF signing
The `CertificationLevel` property was removed from the `PdfSignatureOptions` class. It is now explicitly set to `PdfSignatureCertificationLevel.FormFillingAndAnnotations` in all instances where `PdfSignatureOptions` is instantiated.

Previously, `CertificationLevel` was nullable and dynamically set from the `request` object or defaulted to `PdfSignatureCertificationLevel.NoCertification`. These changes ensure consistent behavior by standardizing the certification level to `FormFillingAndAnnotations`.
2026-10-14 14:31:54 +02:00
68d9dbdcdf Add UUID and metadata support for PDF signing
Enhanced PDF signing with UUID and metadata handling:
- Added `UUID` and `OriginalDocumentSha256` to `PdfSignOptionsDto` and `PdfSignatureCheckItemDto`.
- Introduced `BusinessReason` to separate metadata from signature reasons.
- Updated `PdfDigitalSignatureService` to embed and parse metadata in signature reasons.
- Improved error handling to include metadata in validation results.
- Updated `PdfSignatureController` to handle `UUID` in signing requests.

Refactored PKCS#11 configuration:
- Replaced `LibraryPath` with `AssetsRootPath` for asset resolution.
- Added `ProxySocket` and `DefaultApplicationName` options.
- Clarified runtime behavior for asset resolution in `README.md`.

Updated documentation and examples to reflect these changes.
2026-10-14 14:24:51 +02:00
7f3d1afcb9 Add SoftHSM SSH and SFTP helper scripts 2026-10-14 13:49:56 +02:00
955ac57a1a Add original revision extraction for signed PDFs 2026-10-14 13:49:46 +02:00
b8a96330c0 Add PKCS#11 startup self-test health reporting 2026-10-14 13:49:38 +02:00
8f2744afc7 add soft hsm lib 2026-10-14 13:11:23 +02:00
c8ddfac8d0 Improve PDF signing and validation logic
Updated `.gitignore` to exclude new files and directories, including `FodyWeavers.xsd` and `appsettings.Secrets.json`.

Refactored `PdfDigitalSignatureService`:
- Improved signing time handling by calculating fallback inline.
- Simplified `SignPdfAsync` by inlining signature building.
- Added `ParseByteRanges` to extract `/ByteRange` values from PDFs.
- Introduced `CoversWholeDocument` to validate document coverage.

Enhanced signature validation:
- Integrated `ParseByteRanges` and `CoversWholeDocument`.
- Streamlined `PdfSignatureCheckItemDto` construction.
- Updated properties to calculate values inline.
- Improved `Unchanged` property to check cryptographic validity and document coverage.

These changes enhance maintainability, readability, and functionality.
2026-10-13 18:52:28 +02:00
1942d78155 Add cryptographic infrastructure and configuration
Integrated cryptographic functionality by adding a reference to `EnvelopeGenerator.Infrastructure.Crypt.csproj`. Updated `Program.cs` to include dependency injection for cryptographic services and load `appsettings.Secrets.json` for sensitive configurations. Added a new `Crypt` section in `appsettings.json` to configure PKCS#11 cryptographic operations, enhancing security and signing capabilities.
2026-10-13 14:53:49 +02:00
f361898ba4 Add PKCS#11-based PDF signing infrastructure
Introduced a cryptographic infrastructure for PDF signing and validation using PKCS#11-based hardware security modules (HSMs) or software tokens. Key changes include:

- Added `PdfDigitalSignatureService` for signing PDFs, multi-signature support, and signature validation.
- Implemented `Pkcs11ExternalSigner` and `Pkcs11PdfSigningRsa` for PKCS#11-based signing with DevExpress PDF infrastructure.
- Added utilities like `Pkcs11SessionRunner`, `Pkcs11InteropBootstrap`, and `Pkcs11ToolBridge` for session management, library initialization, and fallback mechanisms.
- Introduced `Pkcs11Options` for managing PKCS#11 configurations.
- Updated project dependencies to include `DevExpress.Document.Processor`, `System.Security.Cryptography.Pkcs`, and `Pkcs11Interop`.
- Enhanced error handling and fallback support for PKCS#11 operations.

These changes enable robust and extensible PDF signing capabilities with detailed signature metadata extraction and validation.
2026-10-13 14:45:47 +02:00
7766e2ea25 Add PKCS#11 support for PDF signing and validation
Introduced `Pkcs11Options` for configuring PKCS#11 signing workflows, including properties for library path, token selection, and signing metadata. Added `PdfSignOptionsDto` to represent signing options and `PdfSignatureCheckResultDto` with related DTOs for signature validation.

Defined `PdfSignatureCertificationLevel` enum for certification levels. Added `IPdfDigitalSignatureService` interface to support signing and validating PDF signatures. These changes enable robust and extensible handling of digital signatures.
2026-10-13 14:44:06 +02:00
6dfcc1b2be Update property accessor and resource management
Modified the `ByteData` property in `DocumentDto.cs` to use a `set` accessor instead of `init`, enabling updates after initialization.

Updated resource manager references in `frmEnvelopeMainData.vb` to use `CommonServices.My.Resources.Model.ResourceManager` for localized string retrieval.
2026-10-08 11:34:05 +02:00
63c60a91b2 Add multi-targeting support for net7.0 and net8.0
Updated the project to support both net7.0 and net8.0 frameworks.
Modified `<TargetFramework>` to `<TargetFrameworks>` and added
conditional `<ItemGroup>` entries for dependencies and project
references specific to each framework. Updated package versions
(e.g., Microsoft.Extensions.DependencyInjection to 9.0.6) and
introduced new dependencies for net8.0, including
Microsoft.EntityFrameworkCore 8.0.17. Added a `<Compile Remove>`
directive for net7.0 to exclude Crypt-related files. Ensured
compatibility and maintained backward support for net7.0.
2026-10-08 10:33:11 +02:00
a5d2c477f6 Add EnvelopeGenerator.Infrastructure.Crypt project
A new project, `EnvelopeGenerator.Infrastructure.Crypt`, has been added to the solution. This project targets `.NET 8.0` and includes several `Microsoft.Extensions` dependencies (e.g., `Configuration.Binder`, `DependencyInjection.Abstractions`, etc.) and the `Pkcs11Interop` package.

The solution file (`EnvelopeGenerator.sln`) has been updated to include the new project, its build configurations, and its nesting under the same parent as `EnvelopeGenerator.Infrastructure.Doc`.
2026-10-08 10:32:48 +02:00
3a71d57619 Enhance FES docs, plans, and SoftHSM integration
Updated README.md for clarity and added official references. Introduced detailed infrastructure and integration plans for FES, including phased implementation, deployment options, and risk mitigations. Added FES workflow diagrams in draw.io and PDF formats. Documented SoftHSM testing process and integration details in ticket.md. Improved alignment with clean architecture principles and production readiness.
2026-10-08 10:31:00 +02:00
73b6c2712c Update ResourceManager references in translation functions
Updated the `TranslateCertificationType` and `TranslateFinalEmailType` functions to use the `ResourceManager` from the `CommonServices.My.Resources.Model` namespace instead of the `Resources.Model` namespace for consistency and improved resource management.
2026-10-08 10:16:22 +02:00
200972c370 Update README with PKCS#11 integration details
Added a detailed README for the `EnvelopeGenerator.Infrastructure.Crypt` project, explaining its purpose, functionality, and integration with PKCS#11-based cryptographic workflows.

Highlights include:
- Overview of the project's role in FES workflows.
- Explanation of SoftHSM and PKCS#11 deployment patterns.
- Mapping of functionality to FES diagram steps 6-8.
- Documentation of service interfaces and DI entry point.
- Configuration guidance with JSON examples and security best practices.
- Instructions for obtaining PKCS#11 values and setting up SoftHSM.
- Checklist for IT handover and environment setup.
- Steps for running cryptographic tests with PowerShell and Linux examples.
- Common failure diagnostics and their meanings.
- Clarification of signing semantics and validation requirements.
- FAQ addressing common questions about SoftHSM and PKCS#11 usage.

This update ensures clear guidance for developers and IT teams to configure and integrate the cryptographic layer effectively.
2026-10-07 13:35:50 +02:00
9f7519941a Improve bootstrap handling in SenderEditorPage
Added a `DxLoadingPanel` to display a loading indicator during the bootstrap process. Introduced the `IsBootstrapLoading` property to track bootstrap progress. Refactored `OnParametersSetAsync` to handle bootstrap tokens more effectively, including redirecting to `/sender` if no token is provided. Updated `TryApplyBootstrapAsync` to adjust navigation behavior when the bootstrap payload is `null`.

Enhanced `OnAfterRenderAsync` to process `_pendingBootstrapToken` more cleanly and removed redundant logic. Simplified navigation and improved code readability and maintainability.
2026-10-06 13:59:39 +02:00
66e58b4a1a Add envelope settings step to wizard
Introduced a new step (step 2) in the `CreateEnvelopeWizardPopup` for configuring envelope settings, including security, reminders, and expiration options.

- Added fields for certification type, final email options, 2FA, access codes, and reminder settings.
- Updated step titles, descriptions, and total steps from 4 to 5.
- Implemented validation logic for the new settings step.
- Added methods to handle changes in settings fields and apply defaults.
- Updated `CreateEnvelopeWizardResult` and `CreateEnvelopeBootstrapPayload` to include new fields.
- Modified `SenderEditorPage` and `SenderPage` to handle new settings.
- Styled the new step with updated CSS for grids, groups, and toggles.
- Refactored state reset logic to initialize new fields.
2026-10-06 12:59:55 +02:00
de9d8c6f37 Add document preview feature to wizard popup
Introduced a document preview feature in the `CreateEnvelopeWizardPopup` component. This includes:

- Injected `PdfRenderService` for rendering PDF pages.
- Replaced document metadata display with a preview card.
- Added `SenderPdfImageEditor` for interactive PDF previews.
- Implemented state management for preview pages and overlays.
- Added fallback logic to estimate PDF page count.
- Enhanced `ResetWizardState` to reset preview-related fields.
- Styled the preview card and wrap with new CSS classes.
- Added placeholder event handlers for editor interactions.
2026-10-06 11:48:03 +02:00
47f48ba12a Add PdfFileDropZone for interactive PDF uploads
Replaced static upload message in `CreateEnvelopeWizardPopup.razor` with a new `PdfFileDropZone` component to enhance user experience. The component supports drag-and-drop functionality, file selection, and dynamic states (e.g., disabled, drag-active).

Introduced `PdfFileDropZone.razor` with customizable parameters such as `Disabled`, `HasDocument`, `UploadText`, and `DragDropHintText`. Added event handling for drag-and-drop interactions and file selection.

Updated `sender-page.css` to include styles for the new component, ensuring seamless integration with the existing layout. Adjusted styles for hover effects, drag-active state, and disabled state.

These changes improve usability and make PDF uploads more intuitive.
2026-10-06 10:31:43 +02:00
60241c9c6d Refactor receiver suggestions filtering logic
Introduced `ReceiverSuggestionFilterHandler` to refine the
filtering of recent receiver suggestions based on name and
email. Added `SuggestionLimit` constant to replace hardcoded
limits and ensure consistency. Updated UI to use the new
`FilteredRecentSuggestions` property, which leverages the
`BuildFilteredRecentSuggestions` method for grouping,
ordering, and limiting suggestions. Adjusted
`OnDocumentSelectedAsync` to use the new constant. Added
necessary `@using` directive for the filter handler.
2026-10-06 09:26:43 +02:00
2a939b4cad Add filtering logic for receiver suggestions
Introduce `ReceiverFilterCriteria` to encapsulate filtering criteria for receivers, including normalization of `FullName` and `EmailAddress`. Add `ReceiverSuggestionFilterHandler` to filter `RecentReceiverSuggestionDto` objects based on the criteria. Includes methods for matching criteria and filtering suggestions.
2026-10-06 09:26:22 +02:00
e89d471c6f Refactor receiver input and improve UI/UX
Replaced receiver list with a tag-based `DxTagBox` for better flexibility and dynamic updates. Updated receiver management logic to synchronize `_receivers` and `_receiverTags`. Enhanced accessibility with ARIA attributes for the suggestion list.

Redesigned the "Add Receiver" button with custom styles, hover effects, and an inline SVG icon. Improved the suggestion list with horizontal scrolling and nowrap styling. Styled tags and added a remove button for each tag.

Increased recent receiver suggestions fetch limit to 20. Added validation to ensure default envelope type selection. Refactored empty state handling and cleaned up redundant code for better maintainability.
2026-10-05 21:58:51 +02:00
ece676fbcf Make Take optional and refactor query handling logic
Updated the `Take` property in `ReadRecentReceiverSuggestionsQuery` to be nullable, allowing for unlimited results when not specified. Refactored the `Handle` method in `ReadRecentReceiverSuggestionsQueryHandler` to conditionally apply the `Take` limit only when provided. Renamed the cancellation token parameter for consistency. Improved code clarity and flexibility by removing redundant operations and dynamically applying the `Take` logic.
2026-10-05 17:06:47 +02:00
fac4ec28a4 Add localization and UI updates for receiver actions
Added new localized strings for "Recently used recipients" and
"Skip" in German, English, and French resource files. Updated
the `CreateEnvelopeWizardPopup.razor` to enhance the "Add
Receiver" button with a primary render style and a new CSS
class. Introduced styles for the button in `sender-page.css`
to improve its appearance with a minimum width, bold font,
and box shadow.
2026-10-05 17:02:12 +02:00
a3b11ed63f Add recent receiver suggestions to wizard
Introduced a feature to display and apply recent receiver
suggestions in the `CreateEnvelopeWizardPopup` component.
Added a new section in step 2 to show recently used receivers,
with loading and state management logic. Updated the
`ReceiverService` to fetch suggestions from the backend API.

Enhanced the UI with new styles for the suggestions section
and improved localization support. Refactored state
management to handle loading, resetting, and applying
suggestions. Minor cleanup and adjustments to ensure proper
state transitions.
2026-10-05 16:53:57 +02:00
5627483589 Make RecentReceiverSuggestionDto immutable; refactor query
Updated RecentReceiverSuggestionDto to use `init` properties,
adding immutability and a new `LastUsedAt` field. Refactored
ReadRecentReceiverSuggestionsQueryHandler to simplify query
logic, remove `IMapper` dependency, and improve determinism
in results. Introduced tie-breaking for duplicate timestamps
and optimized ordering by `LastUsedAt` and `EmailAddress`.
Removed redundant `Include` and mapping steps for clarity.
2026-10-05 16:43:34 +02:00
bf23e88cf8 Add plan for recent receiver suggestions feature
Added a detailed plan document outlining the development of a "recent receiver suggestions" feature for step 3 of the sender wizard. The plan defines team working rules, objectives, functional requirements, and architecture direction across application, server, and client layers.

The implementation is divided into six phases, covering MediatR query creation, API endpoint addition, client integration, UI updates, regression testing, and technical debt tracking. Each phase includes black-box test suggestions to ensure quality and robustness. The plan emphasizes incremental development, user approval, and quality-focused processes.
2026-10-05 15:58:58 +02:00
9435561e78 Add recent receiver suggestions query and DTO
Introduce `RecentReceiverSuggestionDto` to represent recent receiver suggestions with `FullName` and `EmailAddress`.

Add `ReadRecentReceiverSuggestionsQuery` to retrieve recent receiver suggestions scoped by sender username and limited to 3-5 results.

Implement `ReadRecentReceiverSuggestionsQueryHandler` to process the query using Entity Framework Core and AutoMapper.

Update `MappingProfile` to map `EnvelopeReceiver` to `RecentReceiverSuggestionDto`.
2026-10-05 15:55:19 +02:00
db1e91a3f6 Set C# language version to latest in project file
Added the `<LangVersion>` property to `EnvelopeGenerator.Application.csproj` with the value `latest`. This ensures the project uses the most up-to-date C# language features supported by the compiler, enabling access to new enhancements and features as they become available.
2026-10-05 15:32:41 +02:00
0c664c0995 Improve UX, security, and maintainability
- Introduced a DevExpress toast-based notification system using `DxToastProvider` and `IToastNotificationService` for consistent feedback.
- Standardized request-to-domain mapping with AutoMapper/profile conventions or reflection-based mappers, enforced at the Core library level.
- Removed dynamic SQL string composition and migrated to parameterized queries to address SQL injection vulnerabilities. Added CI guardrails to prevent reintroduction.
- Refined UX on `EnvelopeSenderPage.razor` to enforce deterministic row double-click behavior and align row action buttons/tooltips.
- Enabled SPA-style language switching for immediate UI culture updates without page reloads.
- Added sender-scoped caching for "recent receiver suggestions" with short TTL, invalidation hooks, and cross-sender isolation.
2026-10-05 15:20:58 +02:00
5db2f6f1e3 Refactor envelope type selection logic
Removed the placeholder option from the envelope type dropdown to simplify the UI. Introduced a new `GetDefaultEnvelopeTypeId()` method to centralize and enhance the logic for determining the default envelope type. This method prioritizes envelope types with titles containing "Handwritten Signature" if available, improving alignment with user preferences. Updated `_selectedEnvelopeTypeId` initialization to use the new method for better maintainability.
2026-10-05 15:04:09 +02:00
a535e20b0e Add "Next" button translations to localization files
Added a new resource entry for the "Next" button in the
localization files for German (de-DE), English (en-US), and
French (fr-FR). Translations include "Weiter" in German,
"Next" in English, and "Suivant" in French.
2026-10-05 15:01:15 +02:00
bb35a68b1f feat(sender-editor): consume bootstrap payload and auto-save draft
Accept bootstrapToken query parameter and consume one-time payload from sessionStorage-backed service.

Hydrate editor state with title, message, envelope type, receivers, and PDF document from bootstrap data.

Refactor draft save into reusable core flow and trigger non-navigating auto-save after bootstrap is applied.

Prevent stale token replay by clearing query token from URL and handling invalid tokens gracefully.
2026-10-05 14:54:06 +02:00
715add7f51 feat(sender): add create-envelope wizard with bootstrap handoff
Introduce a 4-step modal wizard on SenderPage for creating envelopes.

Validate required fields, support optional skip, receiver management, PDF upload, and keyboard shortcuts.

Persist wizard payload in sessionStorage via token-based bootstrap service and navigate to editor with bootstrapToken.

Register bootstrap storage service in both client and server DI containers and add dedicated sender wizard styling.
2026-10-05 14:53:48 +02:00
621e22701a Add markdown audit and update architecture docs
Added a "Markdown Audit Snapshot" section to document the status, gaps, and recommendations for markdown files in the repository. Included a "Documentation Audit TODO" checklist for tracking updates. Updated the "Active Architecture (Post-Migration)" section to reflect the current Blazor Auto (Server+WASM hybrid) setup. Identified and marked outdated or redundant markdown files for removal or archival. Highlighted the need for periodic synchronization of documentation with implementation progress.
2026-10-05 09:57:56 +02:00
5ef32a2b0c Adjust SenderEditor UI defaults and accordion styling
Changed the `DxAccordionItem` in `SenderEditorPage.razor` to be collapsed by default. Removed unused elements (`sender-settings-chip` and `sender-settings-grid`) from the accordion's content template.

Updated `envelope-viewer.css` to refine the accordion header's appearance: reduced font size, font weight, and padding, and added a `line-height` property for better spacing.
2026-10-02 15:23:57 +02:00
54d16eb3b8 Refactor SenderEditorPage for modernized UI design
Replaced inline styles with reusable CSS classes to improve maintainability, readability, and scalability. Updated the layout and styling of key components, including the action bar, title and message inputs, PDF metadata display, and buttons. Introduced a `DxAccordion` for envelope settings and redesigned the empty state and save result dialog. Added dynamic styling using CSS variables and centralized styles in `envelope-viewer.css`. Removed redundant inline styles and improved overall UI consistency.
2026-10-02 14:59:43 +02:00
ae3264253d Update ILogger type in SenderPage.razor
Changed the `ILogger` dependency injection type parameter from `EnvelopeSenderPage` to `SenderPage` to ensure alignment with the correct class name or intended logging context.
2026-10-02 14:49:38 +02:00
4cbfe52d5b Add localized strings and update localization logic
Added new localized strings to `Model.en.resx`, `Model.fr-FR.resx`,
and `Model.resx` for additional features/settings in English,
French, and German. Updated `SenderEditorPage.razor` to inject
`ModelLocalizer` and modified the `L` function to prioritize
localized strings from `ModelLocalizer` over `UiLocalizer`.
Included `@using EnvelopeGenerator.Domain.Resources` to ensure
access to the `Model` resource file. These changes improve
localization flexibility and maintainability.
2026-10-02 14:46:40 +02:00
27c6b017cd refactor (EnvelopeSenderPage): Rename to SenderPage.razor. 2026-10-02 14:30:11 +02:00
8378c33465 Refactor PDF rendering, UX, and security improvements
Refactored `PdfRenderController.cs` to replace JSON/base64 `byte[]` transport with binary streaming for PDF rendering, improving memory and latency performance for large files.

Introduced a DevExpress toast-based notification system using `DxToastProvider` and `IToastNotificationService` for consistent, actionable feedback.

Standardized request-to-domain mapping with AutoMapper/profile conventions or reflection-based mappers, enforced at the Core library level to reduce drift.

Addressed SQL injection risks by replacing dynamic SQL string composition with parameterized queries, conducted a repo-wide audit, and added CI guardrails.

Improved UX in `EnvelopeSenderPage.razor` by enforcing deterministic row double-click behavior and aligning row action buttons/tooltips.

Implemented SPA-style language switching for immediate UI culture updates without page reloads.

Updated the SQL Server connection string in `appsettings.json`.
2026-10-02 14:04:45 +02:00
c7c4e1194d Force full page reload on language change
Updated the `Navigation.NavigateTo` method in `LanguageControls.razor` to set the `forceLoad` parameter to `true`. This ensures that the application performs a full page reload when the language is changed, improving consistency in language updates.
2026-10-02 13:59:40 +02:00
3b367eff7f Remove EnvelopeSenderEditorPage.razor entirely
The `EnvelopeSenderEditorPage.razor` file was completely removed, including all Razor markup, Blazor components, JavaScript references, CSS styles, and C# code-behind logic.

Key changes:
- Removed the `/sender/editor_legacy` route and all associated imports (`@using`) and dependency injections (`@inject`).
- Deleted the entire HTML structure, including UI elements for PDF editing, signature placement, and receiver management.
- Removed all references to external CSS and JavaScript files.
- Deleted Blazor components like `DxPdfViewer`, `DxPopup`, and `DxTextBox`.
- Removed C# code for state management, lifecycle methods, event handlers, and helper methods.
- Eliminated localization, validation logic, session persistence, and cache management.
- Removed PDF manipulation logic (e.g., placeholder rendering with `PdfSharp`).
- Deleted receiver management logic, including email suggestions and validation.

This change removes the entire functionality of the `EnvelopeSenderEditorPage`, including its UI, backend logic, and external integrations.
2026-10-02 13:39:00 +02:00
d0c3d79fbc Refactor DI, PDF rendering, and improve security/UX
Centralized dependency injection registrations into dedicated
extension methods/modules to reduce startup composition sprawl
and improve maintainability.

Refactored `PdfRenderController.cs` and client service contract
to replace JSON/base64 `byte[]` transport with binary streaming
(`application/octet-stream` or multipart) for improved memory
and latency performance with large PDFs.

Introduced a DevExpress toast-based notification system using
`DxToastProvider` and `IToastNotificationService` for consistent,
actionable feedback across sender/receiver flows.

Standardized request-to-domain mapping with AutoMapper/profile
conventions or reflection/source-generator-based mappers to
eliminate manual mapping drift and enforce consistency across
services.

Addressed SQL injection vulnerabilities by replacing dynamic SQL
string composition with parameterized queries. Conducted a repo-
wide audit and added CI guardrails to prevent reintroduction.

Refined UX for `EnvelopeSenderPage.razor` to enforce deterministic
row double-click behavior (preview OR edit) and aligned row action
buttons/tooltips to eliminate ambiguous navigation.
2026-10-02 13:32:21 +02:00
e81c7896a1 Refactor services, DI, and improve security
- Removed unnecessary request/payload remapping in `Services/` and ensured direct request DTO usage where safe.
- Centralized DI registrations into extension methods/modules to reduce startup sprawl and improve maintainability.
- Refactored `PdfRenderController.cs` to replace JSON/base64 `byte[]` transport with binary streaming for better memory/latency performance.
- Implemented a DevExpress toast-based notification system with `DxToastProvider` and `IToastNotificationService` for consistent feedback flows.
- Standardized request-to-domain mapping using AutoMapper/profile conventions or source-generator-based mappers to eliminate drift.
- Addressed SQL injection risks by replacing dynamic SQL string composition with parameterized queries, conducted a repo-wide audit, and added CI guardrails.
2026-10-02 13:27:09 +02:00
127e2b253e Standardize request-to-domain mapping policies
Introduced a standardized mapping policy to eliminate manual
property-by-property mapping drift (e.g., in
`CreateEnvelopeCommandHandler`). This policy may leverage
AutoMapper with profile conventions or a reflection/source-
generator-based approach and will be enforced across envelope
workflows. The implementation will occur at the Core library
level to ensure consistent guardrails for all services.

Removed the instruction to manually audit redundant mappings
in `EnvelopeGenerator.Server.Client/Services/`, aligning with
the shift toward automated mapping standardization.

Updated the manual testing workflow to include the new mapping
policy requirement, emphasizing its role in improving
consistency and maintainability.
2026-10-02 13:05:02 +02:00
6d0b78072e Refactor error handling in DocumentService
Centralized `HttpRequestException` creation with a new
`CreateHttpException` method to improve code reuse and
consistency. Updated methods to handle `HttpStatusCode.NotFound`
gracefully, returning `null` where appropriate instead of
throwing exceptions. Introduced `TryGetSenderDocumentBytesByEnvelopeIdAsync`
to encapsulate fallback logic for fetching document bytes.
Removed redundant inline exception handling and standardized
error handling across methods.
2026-10-02 13:04:29 +02:00
3721ea9a1f Set envelope status and update title/message fields
Updated `UpdateAsync` to include `e.Title`, `e.Message`, and `e.TfaEnabled` assignments, ensuring database updates reflect request values.
Set `e.Status` to `EnvelopeStatus.EnvelopeSaved` during the update process.
Updated in-memory `envelope` object to reflect changes to `Title`, `Message`, `TfaEnabled`, and `Status` fields.
2026-10-02 13:02:51 +02:00
f86588551c Refactor PDF editor UI and improve PDF loading logic
Simplify and restructure conditional logic in `SenderPdfImageEditor.razor` for better readability and maintainability. Merge redundant `else` blocks and reorganize rendering logic for page previews and fields.

Enhance PDF loading and page preview generation in `SenderEditorPage.razor`. Add error handling for failed page rendering, log warnings, and ensure consistent `_pageCount` initialization based on available data.

These changes improve code clarity, robustness, and maintainability.
2026-10-02 11:51:14 +02:00
0eaa9ff08f Add request validation with FluentValidation
Introduced `ValidationBehavior` to validate MediatR requests using FluentValidation. Registered the behavior in the pipeline and added validators for `ReadDocumentDetailsQuery` and `ReadDocumentQuery`. Updated dependency injection to include validators and added FluentValidation NuGet packages. Improved exception handling with `BadRequestException` for validation failures. Refactored namespaces and imports to support the changes.
2026-10-02 10:57:51 +02:00
ec79d7622e Add support for fetching document metadata separately
Introduced a new `ReadDocumentDetailsQuery` to fetch document
metadata (`DocumentDetailsDto`) without the binary payload.
Updated `MappingProfile` to map `Document` to `DocumentDetailsDto`.

Refactored `DocumentService` to handle separate fetching of
metadata and binary data, combining them into `DocumentDto`.
Changed the metadata endpoint to `/api/Document/details`.

Added a new `GetDocumentDetails` endpoint in `DocumentController`
to return metadata without binary data. Updated
`EnvelopeReceiverPageDataService` with a new method
`GetDocumentMetadataOfSenderAsync` for fetching metadata.

These changes improve efficiency by allowing clients to load
metadata without transferring large binary data when unnecessary.
2026-10-02 10:56:47 +02:00
f558d03bdf Refactor DocumentDto to use DocumentDetailsDto
Introduce a new `DocumentDetailsDto` record to encapsulate shared document metadata and elements without binary payload. Refactor `DocumentDto` to inherit from `DocumentDetailsDto`, eliminating redundant properties and improving code reuse.

Convert `DocumentDto` from a class to a record for immutability. Add a parameterized constructor to `DocumentDto` for initializing binary data alongside metadata. Update `ByteData` to use the `init` accessor.

These changes enhance maintainability, reduce duplication, and enforce immutability by leveraging C# records and inheritance.
2026-10-02 10:55:09 +02:00
db79053c62 Refactor architecture and optimize performance
Applied a modular page architecture to `EnvelopeSenderPage.razor` by splitting large pages into smaller subcomponents, eliminating duplicated code paths, and centralizing shared logic for maintainability.

Conducted a performance review of sender/receiver flows, addressing route transitions, component lifecycle behavior, loading-state correctness, and duplicate API calls. Treated loading logic defects as architectural issues.

Audited client `Services` to remove unnecessary request/payload remapping and simplify HTTP method transformations.

Centralized dependency injection registrations into extension methods/modules to reduce startup composition sprawl.

Refactored `PdfRenderController.cs` to replace JSON/base64 `byte[]` transport with binary streaming for improved memory/latency performance in large PDF rendering.

Implemented a DevExpress toast-based notification system using `DxToastProvider` and `IToastNotificationService` for consistent, actionable feedback across sender/receiver flows.
2026-10-02 10:04:06 +02:00
85dd85aa92 Refactor architecture and optimize PDF rendering
Refactor `EnvelopeSenderPage.razor` to focus on UI composition
and state orchestration, avoiding heavy domain logic. Adopt a
modular page architecture to improve maintainability by splitting
large pages into smaller subcomponents and centralizing shared
logic. Conduct a performance review of sender/receiver flows to
optimize route transitions, component lifecycle behavior, and
loading-state correctness.

Audit `Services` for redundant request/payload remapping and
eliminate unnecessary transformations. Centralize dependency
injection registrations into dedicated extension methods to
reduce startup composition sprawl.

Refactor `PdfRenderController.cs` and its client service contract
to replace JSON/base64 transport for PDF rendering with binary
streaming (`application/octet-stream` or multipart). Add a `TODO`
comment to highlight the need for further optimization of large
PDF handling to reduce payload size and memory pressure.
2026-10-02 09:50:15 +02:00
c7bc9fbcca Refactor EnvelopeType handling and improve validation
Removed AutoMapper dependency in `ReadEnvelopeTypesQueryHandler` and replaced it with manual projection to address null-materialization issues. Updated `EnvelopeType` entity properties to nullable types to align with the database schema. Adjusted computed properties to handle null values gracefully.

Enhanced `SenderEditorPage` with authentication checks, improved handling of reminder and expiry settings, and added validation for user input. Introduced `NormalizeSettings` and `ValidateSettings` methods to ensure consistent and valid configurations. Refactored checkbox value parsing for better reliability.

Injected `AuthService` and `IStringLocalizer` for authentication and localization support. Improved error messaging and localized validation errors. Performed minor code cleanup and refactoring for better readability and maintainability.
2026-10-02 01:25:43 +02:00
1e8e5c96c4 Refactor sender dashboard and improve performance
Moved sender dashboard business/status logic into dedicated
services and extension methods to decouple domain logic from
UI components. Refactored `EnvelopeSenderPage.razor` to focus
on UI composition and state orchestration.

Applied a modular page architecture by splitting large pages
into smaller subcomponents, eliminating duplicated code, and
centralizing shared logic for maintainability.

Conducted a senior-level performance review of sender/receiver
flows, addressing route transitions, loading-state correctness,
and perceived latency issues.

Audited client services for unnecessary request/payload
remapping, removing redundant transformations, and streamlining
HTTP methods. Centralized dependency injection registrations
into extension methods/modules to reduce startup composition
sprawl and improve maintainability.
2026-10-01 16:18:12 +02:00
61d04ca51a Add advanced envelope settings and EnvelopeTypeService
Enhanced `EnvelopeDto` with new properties for advanced settings, including final email options, reminders, and expiration settings. Updated `SenderEditorPage.razor` to include a new UI section for configuring these settings, with dropdowns, checkboxes, and input fields.

Introduced `EnvelopeTypeService` to fetch envelope type data from the backend and applied defaults based on the selected envelope type. Registered the service in `Program.cs` for dependency injection.

Improved lifecycle methods in `SenderEditorPage.razor` to load and apply envelope settings. Enhanced localization, error handling, and data binding for the new settings. Updated `CreateEnvelopeCommand` to include the new properties.

Refactored code for better organization and maintainability, and added logging for debugging envelope type loading issues.
2026-10-01 16:17:34 +02:00
af674a811d Refactor EnvelopeSenderPage and improve architecture
Refactored `EnvelopeSenderPage.razor` to reduce complexity by
moving business and status logic into dedicated services and
extension methods, focusing the component on UI composition
and state orchestration.

Introduced a modular page architecture by splitting large pages
into smaller subcomponents, eliminating duplicated code paths,
and centralizing shared UI/domain logic for better maintainability.

Performed an audit of `EnvelopeGenerator.Server.Client/Services/`
to remove unnecessary request/payload remapping and redundant
transformations, ensuring direct request DTO usage where safe.

Planned a senior-level performance review for sender/receiver
flows to address route transitions, loading-state correctness,
duplicate API calls, and perceived latency issues.

Updated `AGENTS.md` to document manual testing workflow changes
and reflect the refactoring efforts.
2026-10-01 15:53:28 +02:00
123889451f Simplify payload handling in UpsertAsync method
Refactored the `UpsertAsync` method in `EnvelopeService` to remove
manual construction of the HTTP POST payload. The method now
directly uses the `CreateEnvelopeCommand` object as the payload
for the `PostAsJsonAsync` call. This change reduces redundancy,
simplifies the code, and leverages the existing object structure
for better maintainability.
2026-10-01 15:53:10 +02:00
3b5075fe44 Enhance envelope creation and update functionality
Added new optional properties to `CreateEnvelopeCommand` for detailed configuration, including `EnvelopeId`, `Language`, `UseAccessCode`, and expiration settings. Introduced `ResolveEnvelopeSettingsAsync` to centralize settings resolution logic. Updated `CreateEnvelopeCommandHandler` to handle new properties and improve envelope creation/update flow. Added `EnvelopeSettings` record for structured settings management. Enhanced SQL update logic with `UseSQLExecutor` support. Improved handling of language resolution, reminders, and expiration rules.
2026-10-01 15:52:51 +02:00
48302a374f Refactor EnvelopeSenderPage for modularity and performance
Refactored `EnvelopeSenderPage.razor` to reduce complexity by
moving business/status logic into dedicated services and
extension methods. Focused the component on UI composition
and state orchestration, avoiding heavy domain logic in the
Razor file.

Introduced a modular page architecture by splitting large
pages into smaller subcomponents, eliminating duplicated
code paths, and centralizing shared logic for better
maintainability.

Conducted a senior-level performance review for sender/
receiver flows, addressing route transitions, component
mount/unmount behavior, loading-state correctness, duplicate
API calls, and perceived latency. Treated loading logic
defects as architecture/performance issues.
2026-10-01 15:19:13 +02:00
b2c245d6bd Optimize navigation by disabling force page reloads
Updated `Navigation.NavigateTo` calls across multiple files to set `forceLoad` to `false`, preventing unnecessary full page reloads.

- **LanguageControls.razor**: Prevent full reload when switching languages.
- **EnvelopeSenderPage.razor**: Avoid reload during logout navigation.
- **LoginReceiverPage.razor**: Skip reload after successful envelope receiver login.
- **LoginSenderPage.razor**: Prevent reload after successful sender login.

These changes improve performance and enhance user experience by enabling smoother page transitions.
2026-10-01 15:07:37 +02:00
e9c0ec139a Fix null reference issue in DocumentController
Added a null-conditional operator (`?.`) to safely access the
`ByteData` property of the `senderDoc` object, preventing a
potential `NullReferenceException` when `senderDoc` is null.
2026-10-01 12:14:46 +02:00
819bdcfcfb Enhance PDF preview with rendering and editing support
Replaced `DxPdfViewer` with `SenderPdfImageEditor` to enable
PDF rendering, page previews, and field overlay editing.
Added dependency injection for `PdfRenderService` and new
state fields (`_previewPageCount`, `_previewPagePreviews`,
`_previewFileName`, `_previewFieldOverlays`) to manage PDF
metadata and interactions.

Updated `OpenPreviewAsync` to render PDF pages using
`PdfRenderService` and handle errors gracefully. Introduced
`PreviewPdfDataUrl` for generating data URLs and a static
helper `EstimatePdfPageCount` for fallback page count
estimation. Added placeholder methods for user interactions
with the editor.

Improved cleanup logic to reset state fields when the viewer
is unmounted. These changes enhance the PDF preview
functionality while maintaining backward compatibility.
2026-10-01 12:05:56 +02:00
ad7686b7c2 Refactor EnvelopeSenderPage for WebAssembly support
Updated EnvelopeSenderPage to use InteractiveWebAssembly
rendering mode. Removed unused services and authentication
logic, replacing `ReceiverPageDataService` with
`DocumentService` for document retrieval. Improved error
handling by replacing `NotFoundException` with
`HttpRequestException` and a 404 status code check.
2026-10-01 11:42:31 +02:00
ade542ded0 Add method to fetch document bytes by envelope ID
Added `GetSenderDocumentBytesByEnvelopeIdAsync` to the
`DocumentService` class. This method retrieves sender
document bytes using an HTTP GET request to the endpoint
`/api/Document?EnvelopeId={envelopeId}`. It handles HTTP
response errors by throwing an `HttpRequestException` and
supports cancellation via a `CancellationToken`.
2026-10-01 11:42:19 +02:00
01c1283622 Refactor ReceiverPage and update AGENTS.md TODOs
Updated AGENTS.md to add a technical debt TODO for modularizing
server pages by splitting large components, reducing duplication,
and centralizing shared logic.

Enhanced ReceiverPage.razor with new state variables and logic
to support a "Reject" popup feature, including state management
for visibility, submission, and validation.

Improved sharing functionality in ReceiverPage.razor by adding
error handling, user feedback, and proper state updates during
asynchronous operations to enhance maintainability and UX.
2026-10-01 10:08:47 +02:00
d969dbe843 Refactor IPdfPageImageRenderer registration
Encapsulated the registration of IPdfPageImageRenderer into a new
AddDocInfrastructure extension method in the DependencyInjection
class within the EnvelopeGenerator.Infrastructure.Doc namespace.

Updated Program.cs to use the AddDocInfrastructure method, improving
modularity and centralizing the service registration logic.
2026-09-30 17:13:09 +02:00
41779a4f58 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2026-09-30 17:10:54 +02:00
bf739c1360 Improve download/print state handling and toolbar UI
Added `IsDownloading` and `IsPrinting` properties to manage
state during download and print operations in
`SenderPdfImageEditor.razor` and `SenderPdfImageEditorToolbar.razor`.
Updated `OnDownloadPdfAsync` and `OnPrintPdfAsync` to ensure
state cleanup with `finally` blocks.

Enhanced toolbar buttons to reflect busy states with dynamic
titles, disabled states, and a spinning animation. Updated
CSS in `envelope-viewer.css` to improve layout, spacing,
and responsiveness. Added hover, active, and focus-visible
styles for better interactivity. Styled a new zoom slider
with custom thumb and focus states.

Increased maximum width of key elements to 1040px and
adjusted padding/margin for better alignment. Improved
focus styles for input fields and aligned actions to the
right in specific scenarios.
2026-09-30 17:09:33 +02:00
0f167d731e Add PDF printing functionality to editor components
Enabled PDF printing in `SenderPdfImageEditor` and `SenderPdfImageEditorToolbar`. Added `CanPrintPdf` property to determine print availability and a new `OnPrintPdfAsync` method for handling print logic via JavaScript interop. Updated the toolbar with a "Print" button that triggers the print functionality.

Implemented the `printDataUrlPdf` function in `envelope-editor.js` to handle printing from a data URL. This function creates an iframe, loads the PDF, and invokes the browser's print functionality with proper cleanup of resources.
2026-09-30 17:02:05 +02:00
021d7cfe29 Add PDF download functionality to editor components
Introduced the ability to download PDFs in the `SenderPdfImageEditor` component:
- Added `PdfFileName` parameter to specify the file name.
- Implemented `CanDownloadPdf` property to enable/disable downloads.
- Created `OnDownloadPdfAsync` method to handle download logic via JS interop.

Updated `SenderPdfImageEditorToolbar` to support downloads:
- Added `CanDownloadPdf` and `DownloadPdf` parameters.
- Enabled "Download PDF" button based on `CanDownloadPdf`.
- Added `DownloadPdfAsync` method to trigger the download callback.

Enhanced `SenderEditorPage` to pass `PdfFileName` to `SenderPdfImageEditor`.

Added `downloadDataUrlAsFile` function in `envelope-editor.js` to facilitate file downloads using a temporary `<a>` element.

Made minor adjustments for consistency, including adding `ZoomStepPercent` to the toolbar and refining `SetZoomPercent` logic.
2026-09-30 16:48:51 +02:00
e2322b41cd Add zoom functionality to SenderPdfImageEditor
Introduced zoom controls to the `SenderPdfImageEditor` component, including zoom in/out, dynamic zoom levels, and a slider in the toolbar.

- Added zoom-related properties and methods to manage zoom levels.
- Updated the toolbar with zoom buttons and a slider for precise control.
- Enhanced JavaScript to handle zoom overrides and smooth scrolling.
- Modified UI elements to dynamically apply zoom styles.
- Improved accessibility and usability with appropriate attributes.
- Refactored code to integrate zoom functionality seamlessly.

These changes enhance the user experience by providing greater control over PDF viewing and editing.
2026-09-30 16:41:57 +02:00
618451f0c1 resolve merge conflicts 2026-09-30 16:37:16 +02:00
96c7cd6e8a Add "Technical Debt TODOs" section to AGENTS.md
Added a new section in AGENTS.md to document technical debt tasks.
Outlined three specific areas for improvement:
- Refactor `EnvelopeSenderPage.razor` to reduce complexity.
- Move sender dashboard logic to dedicated services/extensions.
- Keep `EnvelopeSenderPage.razor` focused on UI/state orchestration.

These changes aim to improve code modularity, maintainability, and
separation of concerns.
2026-09-30 16:34:06 +02:00
4047ba7c6b Improve envelope status handling and UI enhancements
Refactored status handling to use `GetEffectiveEnvelopeStatus`
for better accuracy and historical context. Updated methods
like `IsEnvelopeSent`, `IsEnvelopeEditable`, and others to
use effective status instead of raw status.

Enhanced receiver status display by adding logic for rejected
receivers (`IsReceiverRejected`) and updated the UI to show
rejection badges with icons and labels. Improved badge
rendering to differentiate between signed, unsigned, and
rejected states.

Improved envelope categorization by ensuring rejected
envelopes are not shown in the active tab. Introduced
`GetEffectiveEnvelopeStatus` to centralize status logic.

Sanitized grid layout filters to prevent persistence of
filter-related properties, ensuring grids appear complete
when restoring layouts. Added utility methods for clearing
filter properties dynamically.

Added comments and refactored code for better readability
and maintainability.
2026-09-30 16:33:19 +02:00
8386b85592 Add PDF navigation to SenderPdfImageEditor
Enhanced SenderPdfImageEditor with page navigation features:
- Added methods to navigate to first, previous, next, last, or specific pages.
- Introduced `_currentPage` to track and validate the current page.
- Implemented `OnParametersSet` to initialize/clamp `_currentPage`.
- Integrated JavaScript interop for scrolling and visible page detection.

Updated SenderPdfImageEditorToolbar:
- Added navigation buttons with enable/disable states.
- Replaced static page display with input and dropdown for page selection.
- Added event callbacks for navigation actions.

Styled new toolbar elements in `envelope-viewer.css` and added:
- Input field and dropdown for page selection.
- Adjusted layout and appearance of navigation controls.

Extended `envelope-editor.js` with:
- `scrollToPdfPage` for smooth scrolling to a specific page.
- `getVisiblePdfPageNumber` to detect the most visible page.
2026-09-30 16:12:38 +02:00
640f0b95fe Add "Notes" section to document pending toolbar polish
The "Approval Gates" section in `SENDER_PDF_TOOLBAR_PLAN_EN.md`
was updated to include a new "Notes" subheading. A checklist
item was added under this section to indicate that visual
polish for the toolbar (e.g., icon sizing, balance, and
spacing adjustments) is still pending. This change provides
clarity on outstanding design tasks.
2026-09-30 15:59:10 +02:00
181f7f4dec Refine UI spacing and dimensions for compact layout
Reduced gaps, padding, and margins across various elements to create a more compact and visually streamlined layout. Adjusted `min-height`, `border-radius`, and `box-shadow` properties for toolbars and buttons to improve consistency. Updated dimensions and font sizes for value boxes and icons to align with the refined design. Applied similar adjustments for resizing states to maintain uniformity.
2026-09-30 15:58:51 +02:00
95961a296f Refactor SenderPdfImageEditor layout and styles
Refactored `SenderPdfImageEditor.razor` to improve readability and maintainability by introducing a new wrapper element (`sender-image-editor-shell`) and reorganizing the toolbar into a sticky container (`sender-image-editor__toolbar-host`). Updated conditional rendering logic to handle `PageCount` cases more effectively.

Enhanced styles in `envelope-viewer.css`:
- Added `.sender-image-editor-shell` for layout structure.
- Made toolbar sticky with `.sender-image-editor__toolbar-host`.
- Improved alignment, spacing, and responsiveness of elements.
- Adjusted padding and margins for better layout consistency.

These changes improve user experience and maintainability.
2026-09-30 15:52:46 +02:00
db613358d5 Add PDF editor toolbar with navigation and zoom controls
Introduced a `SenderPdfImageEditorToolbar` component to enhance the PDF editor with navigation, zoom, and action controls. Updated `SenderPdfImageEditor.razor` to display the toolbar when `PageCount > 0`.

Added parameters (`CurrentPage`, `TotalPages`, `ZoomPercent`) and computed properties to manage and display toolbar state dynamically.

Styled the toolbar in `envelope-viewer.css` with grid-based layout, responsive design, and hover/disabled states for buttons. Adjusted styles for smaller screens to improve usability.
2026-09-30 15:25:29 +02:00
7a126050cf Add migration plan for SenderPdfImageEditor toolbar
Introduce a detailed migration plan to design and integrate a reusable toolbar component for the `SenderPdfImageEditor` in the `EnvelopeGenerator.Server` project.

The toolbar will support page navigation, zoom in/out, PDF download, and print functionality while preserving existing signature add/remove/drag-drop behavior.

The plan includes:
- Creation of a new `SenderPdfImageEditorToolbar` component.
- Updates to `.razor`, `.css`, and `.js` files.
- A non-regression strategy with incremental feature additions and manual black-box testing after each stage.

The migration is divided into five stages:
1. Visual toolbar skeleton.
2. Page navigation.
3. Zoom in/out with mouse override.
4. Download original PDF bytes.
5. Print output.

Approval gates are defined to ensure functionality is implemented correctly and regressions are avoided.
2026-09-30 15:18:25 +02:00
7081c5f64e Refactor PDF editor styles and add throttling logic
Refactored `SenderPdfImageEditor.razor` to use culture-invariant formatting for dynamic styles and introduced the `GetPageCanvasStyle` method for calculating `aspect-ratio` and `cursor` styles. Enhanced layout control with additional CSS properties.

Updated `envelope-editor.js` to include a throttling mechanism for mouse movement events using `_moveThrottle` and `requestAnimationFrame`. Added `getClickCoordsOnPdfPageThrottled` with a promise-based approach to improve performance during rapid interactions.
2026-09-30 15:16:23 +02:00
12ec5b28fa Update cursor behavior in envelope-viewer.css
Changed the `cursor` property for `.sender-image-editor__field`
from `grab` to `move` and for its `:active` pseudo-class
from `grabbing` to `move`. This ensures consistent cursor
behavior when interacting with the element.
2026-09-30 15:15:48 +02:00
925e60a45c Update routing for sender editor pages
Refactored routing logic in `SenderEditorPage.razor` and `EnvelopeSenderEditorPage.razor`:
- Changed the route in `SenderEditorPage.razor` from `/sender/editor2` to `/sender/editor`.
- Updated `EnvelopeSenderEditorPage.razor` to use `/sender/editor_legacy` instead of `/sender/editor`.
- Adjusted `NavigationManager.NavigateTo` calls to reflect the new routes.

These changes separate the functionality of the two pages and clarify their respective roles.
2026-09-30 15:15:30 +02:00
897860b2bf Add rejection handling and dedicated page for envelopes
Introduced a new rejection flow in `ReceiverPage.razor` to detect
rejected envelopes and redirect users to a dedicated rejection
page (`ReceiverRejectedPage.razor`).

- Added `IsRejected` helper method to check envelope rejection
  status based on current status and history.
- Created `ReceiverRejectedPage.razor` to display rejection
  messages, sender details, and navigation options.
- Implemented localized strings for dynamic rejection messages.
- Designed a visually distinct layout for the rejection page.
- Improved user experience with clear messaging and proper
  handling of invalid or unauthorized access.
2026-09-30 14:21:09 +02:00
8933cff558 Add envelope rejection feature with popup UI
Introduced a new feature to allow users to reject envelopes with an optional reason. Added a `RejectAsync` method in `AnnotationService` to handle rejection requests. Updated `ReceiverPage.razor` to include a "Reject" button in the PDF toolbar and a rejection popup for user input.

Implemented state management for the rejection popup, including visibility, submission status, and validation messages. Updated `AnnotationController` to accept `envelopeKey` as a route parameter for the rejection endpoint. Modified `ReceiverClaimExtensions` to support multiple email claim types.

Added new CSS styles for the "Reject" button, including hover effects and dark theme support. Enhanced error handling and logging for the rejection process.
2026-09-30 14:03:56 +02:00
b0b9ba7212 Revert "Update routes and navigation logic in editor pages"
This reverts commit 33c75ea7e1.
2026-09-30 13:49:46 +02:00
33c75ea7e1 Update routes and navigation logic in editor pages
Updated the route for `SenderEditorPage.razor` to `/sender/editor` with `InteractiveWebAssembly` mode. Changed the route for `EnvelopeSenderEditorPage.razor` to `/sender/editorL` with `InteractiveServer` mode. Adjusted navigation logic in `EnvelopeSenderEditorPage.razor` to reflect the new `/sender/editorL` route for `esid`, `editQuery`, and `envelopeId` parameters.
2026-09-30 10:25:05 +02:00
513efa0833 Improve drag-and-drop in SenderPdfImageEditor
Replaced the old drag-and-drop implementation with a custom solution using mouse events for better control and precision. Added `_isDragging` state and dynamic cursor updates to enhance user feedback during drag operations.

Introduced grab offset calculations to ensure accurate field positioning relative to the pointer. Added `OnEditorMouseUp` to reset the drag state and `GetPageSize` to retrieve page dimensions, defaulting to A4 if unavailable.

Updated `envelope-editor.js` to include `pageNumber` in `getClickCoordsOnPdfPage` and removed the obsolete `getDropCoordsOnPage` function. Enhanced `EditorCoords` to include `PageNumber` for improved page identification.
2026-09-30 09:20:41 +02:00
0c81d79a9b Add drag-and-drop support for PDF field repositioning
Introduced drag-and-drop functionality for fields in the PDF editor:
- Added `SenderPdfFieldMoveRequest` record to encapsulate field movement data.
- Enhanced `SenderPdfImageEditor.razor` with drag-and-drop event handlers (`OnFieldDragStart`, `OnPageDropAsync`) and updated field structure.
- Updated `SenderEditorPage.razor` to handle field movement via the `OnMoveFieldAsync` method.
- Improved CSS styles for draggable fields and added a delete button with hover effects.
- Added `getDropCoordsOnPage` function in `envelope-editor.js` to calculate normalized drop coordinates.

These changes improve the user experience by enabling intuitive field placement and management in the PDF editor.
2026-09-30 09:08:17 +02:00
696cd1b623 Enhance PDF upload UX with progress and state handling
Refactor the PDF upload button to dynamically adjust its
appearance and behavior based on `_isPageReady` and
`_isPdfUploadInProgress` states. Add a spinner to indicate
upload progress and disable the `<InputFile>` component
during uploads or when the page is not ready.

Introduce `_isPdfUploadInProgress` state to prevent multiple
uploads and ensure consistent UI updates. Update the
`OnPdfFileSelectedAsync` method to handle this state,
including a `finally` block to reset it and trigger UI
updates.

Consolidate redundant code for disabled states and improve
conditional rendering for better maintainability. Enhance
visual feedback for loading states and make minor style
adjustments for consistency.
2026-09-29 22:18:25 +02:00
2434f67627 Add conditional rendering for PDF upload readiness
Introduced `_isPageReady` state to manage page readiness and conditionally render the PDF upload button. Disabled the upload functionality and added a loading spinner when the page is not ready. Updated `OnPdfFileSelectedAsync` to prevent uploads before readiness.

Modified the "No Document Loaded" section to use `_isPageReady` for conditional rendering. Added `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` to the solution. Improved user experience with better feedback during loading states.
2026-09-29 21:41:03 +02:00
776700b247 feat(sender-editor): use rendered page metadata for field placement 2026-09-29 19:22:01 +02:00
7a114f47a6 feat(sender-editor): add PDF page rendering pipeline 2026-09-29 19:21:51 +02:00
ed44d841e7 Refactor PDF editor with custom rendering
Replaced `DxPdfViewer` with a custom PDF rendering solution in `SenderPdfImageEditor` to improve flexibility and control. Added support for displaying placeholder messages and interactive signature fields. Introduced the `SenderPdfFieldOverlay` record to encapsulate field data.

Updated `SenderEditorPage` logic to manage page counts, image URLs, and field overlays. Added a method to estimate PDF page counts. Refactored field removal logic to use index-based operations.

Enhanced styling in `envelope-viewer.css` for improved layout and responsiveness. Updated localization files with new strings for page labels and placeholders. Adjusted JavaScript selectors in `envelope-editor.js` to support the new rendering approach.
2026-09-29 16:01:42 +02:00
4bd0cc2e94 Remove DevExpress PDF viewer dependency
Transitioned Sender Editor away from DevExpress PDF viewer, introducing alternative solutions for PDF rasterization. Enforced new localization requirements for UI text using `IStringLocalizer<Resource>` with translations in DE/EN/FR resource files.

Ensured data compatibility for `CreateEnvelopeCommand` payload shape, field units, receiver-field mapping, and save/send behavior. Removed `esid` query parameter and `IMemoryCache`-based session persistence.

Prohibited use of `pdf.js` and DevExpress PDF viewer, requiring an MIT-licensed alternative if DevExpress rasterization is unsuitable. Maintained adherence to Clean Architecture layers and MediatR pattern. All code and comments are now standardized in English.
2026-09-29 15:17:56 +02:00
8a4963472c Add PDF editing and envelope management features
Introduced a new `SenderEditorPage` with a modern UI for managing envelopes, uploading PDFs, and placing signature fields. Integrated `DevExpress.Blazor.PdfViewer` for PDF rendering and interaction.

Key features include:
- PDF upload, rendering, and validation.
- Signature field placement with normalized coordinates.
- Receiver management with validation and email suggestions.
- Envelope metadata handling (title, message).
- Save as draft and send functionality with validation.
- Edit mode for existing envelopes.
- Improved UX with dynamic elements and color-coded receivers.
- Localization support for multi-language UI.

Updated `_Imports.razor` to include necessary namespaces for DevExpress components and the new `PdfEditor` namespace.
2026-09-29 11:59:06 +02:00
4d3f72a3ed Add ReceiverService and enhance DocumentService
Added `ReceiverService` to the dependency injection container in `Program.cs` and implemented its functionality for fetching receiver email suggestions via API.

Enhanced `DocumentService` with a new method `GetSenderDocumentDetailsByEnvelopeIdAsync` to fetch sender document details by envelope ID. Introduced JSON handling improvements with `JsonSerializerOptions`.

These changes improve support for receiver-related operations and extend document-related capabilities.
2026-09-29 11:58:29 +02:00
1ebd43e3b0 Remove envelope-related PDF viewer components
The following files have been removed as part of a significant refactor or deprecation of envelope-related PDF viewing and signature functionality:

- `EnvelopeReceiverPage.razor`: A detailed PDF viewer with signature handling and interactive features.
- `EnvelopeReceiverPage_DxPdfViewer.razor`: A simplified PDF viewer using the `DxPdfViewer` component.
- `EnvelopeReceiverPage_DxReportViewer.razor`: A report viewer using the `DxReportViewer` component.
- `EnvelopeReceiverPage_embed.razor`: A minimalistic PDF viewer using the `<embed>` HTML tag.

These removals indicate a transition to a new approach for handling PDF viewing and related features or their complete deprecation.
2026-09-29 09:55:13 +02:00
19aeda5aef Add migration plan for WASM-based PDF editor component
A detailed migration plan has been added to replace the DevExpress PDF viewer in `EnvelopeSenderEditorPage.razor` with a new WebAssembly (WASM)-based PDF editor component.

The plan outlines the scope, constraints, and implementation strategy, ensuring feature parity, backend compatibility, and minimal disruption to the existing user experience. Key decisions include creating a reusable `SenderPdfImageEditor` component, using server-side PDF rasterization, and removing `esid` and `IMemoryCache`-based session persistence.

The document also defines testing and acceptance criteria to verify the migration's success, focusing on visual fidelity, functionality, and backend contract consistency.
2026-09-29 09:41:09 +02:00
940d129c11 Add read-only sharing and viewing functionality
Introduced the ability to share envelopes in read-only mode and view them via a dedicated page. Added `ShareEnvelopeReadOnlyAsync` in `AuthService.cs` to handle sharing logic. Updated `ReceiverPage.razor` with a "Share" button, popup UI, and validation for sharing inputs. Created `ReceiverReadOnlyPage.razor` for read-only document viewing with error handling and lifecycle management.

Added `EnvelopeKeyRedirectController.cs` to support legacy link redirection. Enhanced `ReadOnlyController.cs` to support envelope sharing with improved logging and `IMediator` integration. Improved state management, validation, and error handling across components. Performed general code cleanup and UI enhancements for better user experience.
2026-09-28 14:42:52 +02:00
44bbdbb835 Add logout functionality and signed status check
Introduced a logout feature to the `ReceiverPage` component:
- Added a logout button to the toolbar with `_isLoggingOut` state.
- Implemented `LogoutAsync` method to handle logout logic, including navigation and error handling.
- Injected `IMediator` and `ILogger<ReceiverPage>` for logging and MediatR functionality.
- Added a check in `OnInitializedAsync` to redirect users to `/envelope/signed-result` if the envelope is already signed.
- Updated namespaces to include MediatR and custom extensions.

These changes improve user experience and enhance the component's functionality.
2026-09-28 14:11:06 +02:00
223017006b Update launch URLs in launchSettings.json
Updated the `applicationUrl` property in the "https (Blazor UI)"
profile of `launchSettings.json`. Changed the HTTPS port from
8089 to 8088 and the HTTP port from 5132 to 5131. This ensures
the development server uses the new port configuration and
avoids potential conflicts with other services.
2026-09-28 13:59:44 +02:00
ad9250c7b1 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator
# Conflicts:
#	EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
#	EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
2026-09-28 13:53:40 +02:00
5cc47ca9c7 Add signed result page and update navigation flow
Introduced a new `/envelope/signed-result` page to confirm
successful document signing with localized messages and
a visually appealing UI. Updated `ReceiverSignedPage.razor`
to redirect users to the new page after signing.

Enhanced styles in `app.css` for the signed result page,
including responsive layout, theme-specific designs, and
typography improvements. Added support for localization
using `IStringLocalizer` with fallback strings in German.

Improves user experience by providing clear feedback and
a polished confirmation page after signing.
2026-09-28 13:46:48 +02:00
156988403a Add envelope receiver access code request feature
Introduced a feature to allow envelope receivers to request an
access code via email on the login page.

- Added `OnInitializedAsync` in `LoginReceiverPage.razor` to
  trigger access code requests using `AuthService`.
- Implemented `RequestEnvelopeReceiverAccessCodeAsync` in
  `AuthService.cs` to handle API calls for access code requests.
- Added a new endpoint in `AuthController` to process access
  code requests, including validation, history recording, and
  email dispatching.
- Registered `IEnvelopeMailService` and `AddDispatcher` in
  `Program.cs` to support email dispatch and database operations.
- Refactored `AuthController` constructor for readability and
  added necessary `using` directives.
- Improved logging and error handling for better traceability
  and robustness.

These changes ensure a seamless and secure process for envelope
receivers to request access codes, enhancing the user experience.
2026-09-28 13:35:32 +02:00
e960c02cd4 Add AccessCode feature to EnvelopeSenderPage UI
Introduced a new `AccessCode` property in `EnvelopeReceiverDto` to support displaying and copying access codes in the envelope sender page.

- Updated `EnvelopeReceiverDto` to include the nullable `AccessCode` property.
- Enhanced `EnvelopeSenderPage.razor` to display the `AccessCode` for each receiver, with a copy-to-clipboard button.
- Implemented `CopyAccessCodeAsync` for clipboard functionality using JavaScript interop.
- Styled the `AccessCode` section in `sender-page.css` for a responsive and user-friendly design.
- Improved error handling for clipboard operations with console warnings.

These changes improve the user experience by allowing senders to view and copy access codes for receivers.
2026-09-28 12:47:37 +02:00
a0e2492757 Add email queuing for envelope invitation process
Introduced functionality to queue and send invitation emails to envelope receivers. Added new dependencies and private properties to support email handling, including repositories for `EmailTemplate` and `EmailOut`, as well as configuration services for `MailParams` and `DispatcherParams`.

Added the `QueueInvitationEmailsAsync` method to generate personalized email content using placeholders and store outgoing emails in the `EmailOut` repository. Introduced a helper method `ReplacePlaceHolders` for placeholder substitution in email templates.

Updated the `Handle` method to invoke `QueueInvitationEmailsAsync` after setting the envelope status to `EnvelopeQueued`. These changes ensure a modular and reusable email queuing process.
2026-09-28 12:35:51 +02:00
28f8b829e3 Refactor: Unify grid logic for active/completed tabs
Replaced separate `DxGrid` components for "active" and "completed" tabs with a single unified grid. Introduced dynamic properties (`CurrentGridData` and `CurrentGridKey`) to handle data and layout switching based on the active tab.

Refactored layout loading/saving methods into shared versions (`OnGridLayoutAutoLoading` and `OnGridLayoutAutoSaving`) using dynamic keys. Simplified export logic to use the unified grid instance.

Removed redundant code, templates, and private fields specific to the "completed" tab. Updated status filter logic to dynamically fetch data. Improved maintainability by consolidating logic and reducing duplication.
2026-09-28 12:06:35 +02:00
d00c459124 Optimize query with eager loading for related entities
Added `Include` statements to the LINQ query in the
`ReadEnvelopeQueryHandler` class to eagerly load related
entities (`Type`, `EnvelopeReceivers` with `Receiver`, and
`Histories`). This change reduces lazy-loading overhead and
ensures all necessary data is retrieved in a single query,
improving performance.
2026-09-28 11:57:41 +02:00
287c14e114 Refactor signed status logic in EnvelopeSenderPage
Introduce `GetSignedReceiverCount` and `IsReceiverSigned` helper methods to centralize and improve the logic for determining the signed status of receivers. Replace inline logic with these methods for better encapsulation and reusability.

Update UI rendering logic to dynamically determine receiver badge states based on the new methods. These changes enhance code maintainability, reduce duplication, and ensure consistent behavior across the application.
2026-09-28 11:55:31 +02:00
212ba9d569 Enhance grid filtering and sorting functionality
Updated `DxGrid` components for `_activeEnvelopes` and `_completedEnvelopes`:
- Disabled `ShowFilterRow` and added `FilterMenuButtonDisplayMode` to always display filter menu buttons.
- Enabled sorting for the `Id` column with descending order and added a display format.
- Added `EditSettings` to the `Status` column with a `DxComboBoxSettings` for filtering, using the new `GetStatusFilterItems` method.

Introduced `GetStatusFilterItems` to generate distinct status filter items and added a private `StatusFilterItem` record type for encapsulating status values and labels.

Made minor adjustments to grid layout auto-saving logic for consistency.
2026-09-28 11:40:22 +02:00
735f072dd1 Add localization support for multi-language UI
This commit introduces comprehensive localization support for the application, enabling dynamic language switching and localized UI elements. Key changes include:

- Added `.resx` files for German (de-DE), English (en-US), and French (fr-FR).
- Updated Razor components to use `IStringLocalizer` for dynamic string localization.
- Introduced the `L(string key, string fallback)` helper method for consistent localized string retrieval with fallback support.
- Implemented dynamic culture initialization in `Program.cs` via an API call to `/api/Localization/lang`.
- Localized error pages, grid columns, tabs, popups, validation messages, and tooltips.
- Dynamically generated privacy policy links based on the current culture.
- Improved user experience with fully localized loading states, error handling, and UI labels.

These changes enhance accessibility and usability, making the application more user-friendly for a global audience.
2026-09-28 10:46:06 +02:00
48cfd3f416 Add French localization resource file
Added `Model.fr-FR.resx` to support French localization.
Defined XML schema for the resource file structure, including
metadata, assembly, and data elements. Added localized
French strings for various application messages, ensuring
proper display when the locale is set to `fr-FR`.
2026-09-28 09:40:56 +02:00
0d2e43b2d6 Add multilingual support with language selector
Introduced a `LanguageControls` component for switching between
languages (`de-DE`, `en-US`, `fr-FR`) and integrated a new
`LocalizationService` to manage language preferences. Updated
`LocalizationController` to handle language persistence via
cookies and support multiple cultures. Enhanced the footer
layout to include the language selector alongside theme
controls.

Added `flag-icons` library for displaying country flags and
configured request localization in `Program.cs`. Styled the
language selector dropdown and improved sender page visuals
with updated CSS. Defaulted language handling to `de-DE` for
fallback scenarios. Cleaned up redundant code in
`LocalizationController`.
2026-09-28 09:19:46 +02:00
5aaa37b8ec Default localization to "de-DE" and refactor resources
Added a new `Model` class in the `EnvelopeGenerator.Domain.Resources` namespace as a marker for `IStringLocalizer` resource lookup. Updated `EnvelopeSenderPage.razor` to use the new `Model` class for localization.

Modified `LocalizationController` to default to "de-DE" when no culture is specified or found. Refactored localization setup in `Program.cs` to use a fixed default culture of "de-DE" and persist culture via cookies. Removed dynamic culture setup and `QueryStringRequestCultureProvider`.

Performed minor cleanup in `LocalizationController` and `Program.cs` to align with the new localization approach.
2026-09-28 08:46:11 +02:00
051a8ce211 Add history display and enhance status handling
Introduced a "Historie" section in `EnvelopeSenderPage.razor` to display envelope history with status badges, actor info, and timestamps. Updated `ReadEnvelopeQueryHandler` to include histories in queries.

Refactored `GetStatusInfo` to use localized labels and expanded supported statuses. Added helper methods for history processing and display.

Enhanced UI with new styles for the history section in `sender-page.css` and improved responsive design. Added dependency injections for localization and history handling.
2026-09-28 08:39:39 +02:00
519a8bf3f9 Refactor sender resolution and add history logging
Refactored sender email resolution into a new private async
method `ResolveSenderReferenceAsync` to improve code
reusability and readability. Replaced inline logic for
resolving sender references with this method.

Added a `CreateHistoryCommand` to log envelope creation
events, including envelope ID, sender reference, and status
(`EnvelopeCreated`). This enhances auditability and ensures
consistent event logging.
2026-09-27 20:20:01 +02:00
a7d0b40e39 Refactor DocReceiverElement handling and add draft workflow
Removed `IDocReceiverElementWriter` interface and its implementation, replacing it with repository-based operations (`IRepository<DocReceiverElement>`). Updated `CreateEnvelopeCommandHandler` to use repository methods for deleting and creating `DocReceiverElement` records.

Cleaned up dependency injection by removing `IDocReceiverElementWriter` registration. Added new database triggers for `DocReceiverElement` in `appsettings.json` and `appsettings.Database.json`.

Introduced `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` to outline the migration of the sender save/edit workflow to Blazor Server. This includes splitting save and send actions, supporting draft persistence, and enabling draft editing from the dashboard.

Removed unused namespaces and performed code cleanup. Updated testing strategies to ensure correctness and prevent regressions.
2026-09-27 19:56:02 +02:00
3dc9a180d3 docs(agents): align sender workflow endpoint guidance with upsert API
- replace outdated draft/update/send endpoint list with current POST /api/Envelope workflow contract
2026-09-26 13:11:50 +02:00
4dba965c75 feat(sender): route editor save and send through unified envelope upsert API
- expose POST /api/Envelope for sender-authenticated create/update/send operations

- replace client draft-specific call with EnvelopeService.UpsertAsync(CreateEnvelopeCommand)

- update sender editor save/send flow to persist draft first and send by envelope id

- allow editor hydration without document by making sender document fetch nullable
2026-09-26 13:11:44 +02:00
4d2538cc2c refactor(envelope): consolidate draft flow into upsert command
- extend CreateEnvelopeCommand to support draft updates and send intent

- merge draft save logic into CreateEnvelopeCommandHandler and remove duplicate handler

- add DocReceiverElement writer abstraction and infrastructure registration for SQL-based field upserts
2026-09-26 13:11:28 +02:00
d201eacc97 Unify Blazor frontend and API into EnvelopeGenerator.Server
The `EnvelopeGenerator.Server` project now serves as the unified runtime host for both the Blazor frontend and Web API backend, deprecating the `EnvelopeGenerator.API` project. All API endpoints have been merged into `EnvelopeGenerator.Server`.

Key changes:
- Updated `EnvelopeGenerator.Server` to include API controllers.
- Deprecated `EnvelopeGenerator.API` and removed it from the active solution.
- Simplified development workflow to run only `EnvelopeGenerator.Server`.
- Updated `appsettings.json` to consolidate configuration settings.
- Adjusted documentation to reflect the new unified architecture.
- Updated manual testing steps and environment variable references.
- Migrated legacy VB.NET app references to the new architecture.

These changes simplify development, deployment, and testing workflows while consolidating the solution architecture.
2026-09-25 14:26:16 +02:00
9daa671655 chore(solution): remove API project entry from solution 2026-09-25 14:22:08 +02:00
3fbf011fe3 refactor(envelope): update delete handler with explicit field update 2026-09-25 14:21:56 +02:00
a815705fa4 feat(sender): support draft editing flow in sender pages 2026-09-25 14:21:23 +02:00
6e44d0d4b5 feat(sender): add envelope draft save command and endpoint 2026-09-25 14:21:08 +02:00
42a853cb0d refactor(API): removed because migrated to the server-application 2026-09-25 12:56:46 +02:00
b46c3c8ebe feat(sender): add envelope delete popup and client call 2026-09-25 11:31:53 +02:00
9f712a6b8c test: cover delete envelope command scenarios 2026-09-25 11:31:47 +02:00
4449743105 feat: add envelope delete command and API endpoint 2026-09-25 11:31:40 +02:00
184 changed files with 14827 additions and 3145 deletions

1
.gitignore vendored
View File

@@ -370,3 +370,4 @@ FodyWeavers.xsd
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output /EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md /EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/ignore/EnvelopeGenerator.Form /ignore/EnvelopeGenerator.Form
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json

142
AGENTS.md
View File

@@ -48,6 +48,16 @@ This section explains every markdown file in the repo: what it contains, why it
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation - Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified. **Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
#### `DATABASE_STRUCTURE_CONTEXT.md`
**What:** Deep DB documentation for the envelope domain. Includes table/column snapshot, entity-to-table mapping, EF relationship/constraint map, stored procedure behavior + call sites, and trigger behavior + wiring path.
**Why it exists:** DB is centralized and can evolve independently; this file preserves migration-critical data semantics when historical DB context is hard to recover.
**When you need it:**
- Planning or reviewing DB/schema changes
- Debugging status transitions/history side effects
- Tracing where `PRSIG_*` procedures or DB triggers affect app behavior
- Reconciling Domain entities with physical SQL schema
**Maintenance:** Refresh on each DB migration, trigger/procedure change, or significant Domain/EGDbContext model change.
--- ---
### Deployment Docs ### Deployment Docs
@@ -63,15 +73,45 @@ This section explains every markdown file in the repo: what it contains, why it
--- ---
## Markdown Audit Snapshot (2026-10-05)
The following table summarizes repository markdown files by practical status: completed, partially complete, or removable candidates.
| File | Type | Status | Missing / Gap | Removal Recommendation |
|---|---|---|---|---|
| `AGENTS.md` | Agent onboarding | Partially complete | Route/migration statements can drift from real code over time. | Keep |
| `COPILOT_CONTEXT.md` | Architecture reference | Partially complete | Some route/render details may be out of sync with other docs; needs regular reconciliation. | Keep |
| `FORM_APPLICATION_CONTEXT.md` | Legacy-to-modern feature spec | Partially complete | Large sections still marked not implemented / needs implementation. | Keep |
| `RECEIVER_PDF_VIEWER_CONTEXT.md` | Receiver PDF viewer deep-dive | Partially complete | Long migration plan needs periodic sync with active implementation. | Keep |
| `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` | Sender workflow implementation plan | Partially complete | No explicit implementation progress markers per phase. | Keep (until migration closes) |
| `SENDER_PDF_TOOLBAR_PLAN_EN.md` | Sender PDF toolbar plan | Incomplete | Explicit pending item remains (`Toolbar visual polish`). | Keep (until done) |
| `SENDER_EDITOR_WASM_PDF_EDITOR_IMPLEMENTATION_PLAN_en.md` | Sender editor WASM migration plan | Incomplete | Plan exists but completion/progress tracking is minimal. | Keep (until done) |
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md` | Deployment guide | Complete (operational) | No critical gap identified. | Keep |
| `envelope-generator-ui/README.md` | Angular scaffold README | Complete but generic | Template text only; low project-specific value. | Removable or replace with project-specific summary |
| `EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Third-party vendor README | Complete (vendor) | Not project workflow documentation. | Keep with vendor assets |
| `EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Third-party license | Complete (legal) | No gap. | Keep |
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md` | Third-party vendor README | Complete (vendor) | Not project-specific guidance. | Keep with vendor assets |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/README.md` | Legacy PSPDFKit note | Outdated | Mentions deprecated PSPDFKit-based flow; may mislead if reused. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Duplicate vendor README | Redundant | Duplicate copy under `ignore/`. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/bootstrap-icons/README.md` | Vendor README in archive area | Secondary | Archive-only utility, not active docs. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Duplicate license in archive area | Redundant | Duplicate copy under `ignore/`. | Removable/archive (if legal policy allows) |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/README.md` | Duplicate Angular scaffold README | Redundant | Generic template + duplicate in archive path. | Removable/archive |
### Documentation Audit TODO
- [ ] Validate this markdown audit table against the current codebase and update statuses (`complete`, `partial`, `incomplete`, `removable`) when implementation changes land.
---
## Active Architecture (Post-Migration) ## Active Architecture (Post-Migration)
**Frontend:** Blazor Auto (Server+WASM hybrid) **Frontend:** Blazor Auto (Server+WASM hybrid)
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend - **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic - **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0) **Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
**Proxy:** YARP in EnvelopeGenerator.Server routes `/api/*` → `localhost:8088` (API) **Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
### Deprecated Projects - DO NOT USE ### Deprecated Projects - DO NOT USE
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server) - `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
@@ -80,18 +120,14 @@ This section explains every markdown file in the repo: what it contains, why it
## Development Commands ## Development Commands
### Run Both Projects (Required) ### Run Active Host
```powershell ```powershell
# Terminal 1 - API Backend # Terminal - Blazor + Web API host
cd EnvelopeGenerator.API
dotnet run
# Terminal 2 - Blazor Frontend
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
dotnet run dotnet run
``` ```
**Critical:** Both must run simultaneously. EnvelopeGenerator.Server proxy forwards `/api/*` to API. **Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
### Build ### Build
```powershell ```powershell
@@ -104,10 +140,10 @@ dotnet build EnvelopeGenerator.sln
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.) EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers) EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
EnvelopeGenerator.API/ # Controllers, endpoints
EnvelopeGenerator.Server/ EnvelopeGenerator.Server/
EnvelopeGenerator.Server/ # Server-side Blazor components EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
├─ Components/Pages/ # @rendermode InteractiveServer ├─ Components/Pages/ # @rendermode InteractiveServer
├─ Controllers/ # Active Web API endpoints
EnvelopeGenerator.Server.Client/ # Client-side WASM components EnvelopeGenerator.Server.Client/ # Client-side WASM components
├─ Pages/ # @rendermode InteractiveWebAssembly ├─ Pages/ # @rendermode InteractiveWebAssembly
├─ Services/ # HTTP API clients ├─ Services/ # HTTP API clients
@@ -156,15 +192,13 @@ float inches = (pixelX / canvasWidth) * pageWidthInches;
- **Use case:** External API consumers - **Use case:** External API consumers
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates) - **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
### Missing Granular Endpoints (Need to Create) ### Sender Workflow Endpoint
``` ```
POST /api/Envelope/draft # Create draft envelope POST /api/Envelope # Create/update/save/send envelope workflow payload
PUT /api/Envelope/{id} # Update metadata
DELETE /api/Envelope/{id} # Delete with reason DELETE /api/Envelope/{id} # Delete with reason
POST /api/Envelope/{id}/document # Upload PDF POST /api/Envelope/{id}/document # Upload PDF
POST /api/Envelope/{id}/receivers # Add receiver POST /api/Envelope/{id}/receivers # Add receiver
POST /api/Envelope/{id}/signature-fields # Place signature field POST /api/Envelope/{id}/signature-fields # Place signature field
POST /api/Envelope/{id}/send # Send to receivers
``` ```
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements. See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
@@ -199,7 +233,7 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
} }
``` ```
### API Config (`API/appsettings.json`) ### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
- `ConnectionStrings:Default` - SQL Server DB - `ConnectionStrings:Default` - SQL Server DB
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`) - `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
- `Cache:SignatureCacheExpiration` - Signature persistence timeout - `Cache:SignatureCacheExpiration` - Signature persistence timeout
@@ -237,27 +271,84 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
**No automated tests exist yet.** **No automated tests exist yet.**
Manual testing workflow: Manual testing workflow:
1. Start API (`dotnet run` in `EnvelopeGenerator.API`) 1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
2. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`) 2. Navigate to `https://localhost:5131` (or check console output for port)
3. Navigate to `https://localhost:5131` (or check console output for port) 3. Test sender login at `/sender/login`
4. Test sender login at `/sender/login` 4. Test receiver flow at `/envelope/login/{envelopeKey}`
5. Test receiver flow at `/envelope/login/{envelopeKey}`
## Technical Debt TODOs
- **CRITICAL (Licensing / pre-commercial release):** Remove all `SixLabors.ImageSharp` and `QRCoder-ImageSharp` package dependencies from production/runtime projects before commercial release. Replace remaining QR/image paths with permissive-license alternatives (no copyleft/share-source obligations), run repo-wide package audit (`*.csproj`), and add CI guardrails to block reintroduction.
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
- Implement SPA-style language switching so the UI culture updates immediately without a full page reload.
- Implement sender-scoped caching for "recent receiver suggestions" used during envelope creation (wizard/editor flows): cache key should include sender identity, support short TTL + invalidation hooks after successful envelope receiver writes, and prevent cross-sender suggestion leakage.
## Database ## Database
**SQL Server** (DD_ECM) **SQL Server** (DD_ECM)
- Connection string in `API/appsettings.json` - Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
- EF Core migrations NOT used (manual SQL scripts) - EF Core migrations NOT used (manual SQL scripts)
- Stored procedures: `PRSIG_*` prefix - Stored procedures: `PRSIG_*` prefix
**Key Tables:** **Key Tables:**
- `TBSIG_ENVELOPE` - Envelope metadata - `TBSIG_ENVELOPE` - Envelope metadata
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments - `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
- `TBSIG_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES) - `TBSIG_DOCUMENT_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
- `TBSIG_RECEIVER` - Receiver registry - `TBSIG_RECEIVER` - Receiver registry
- `TBSIG_DOCUMENT` - PDF binary data - `TBSIG_ENVELOPE_DOCUMENT` - PDF binary data
- `TBSIG_ENVELOPE_HISTORY` - Audit trail - `TBSIG_ENVELOPE_HISTORY` - Audit trail
### Data Structure (Domain + EGDbContext)
Use this as the canonical quick map before touching sender/receiver workflow code.
#### Core Aggregate Shape
- `Envelope` (`TBSIG_ENVELOPE`) is the root aggregate for sending workflow.
- `Envelope -> Documents` is `1:N` via `Document.EnvelopeId` (`TBSIG_ENVELOPE_DOCUMENT`).
- `Envelope -> Histories` is `1:N` via `History.EnvelopeId` (`TBSIG_ENVELOPE_HISTORY`).
- `Envelope <-> Receiver` is `N:N` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{EnvelopeId, ReceiverId}`).
- `Document -> DocReceiverElement` is `1:N` via `DocReceiverElement.DocumentId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT`).
- `DocReceiverElement -> ElementAnnotation` is `1:N` via `ElementAnnotation.ElementId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`).
#### Status and Tracking Entities
- `DocumentStatus` (`TBSIG_DOCUMENT_STATUS`) links `EnvelopeId + ReceiverId` and stores receiver-level signing state.
- `History` stores timeline events (`EnvelopeStatus`) with polymorphic `UserReference` lookup semantics.
- `EnvelopeReceiverReadOnly` (`TBSIG_ENVELOPE_RECEIVER_READ_ONLY`) stores temporary read-only delegation with validity window (`DateValid`).
#### Important Constraints From `EGDbContext`
- `Envelope.Uuid` has a unique index.
- `Receiver.Signature` has a unique index.
- `Receiver.EmailAddress` has a unique index.
- `EnvelopeReceiver` uses composite primary key: `{ EnvelopeId, ReceiverId }`.
- `History.UserReference` is mapped conditionally (`IsMigration == false`) to:
- `Receiver.EmailAddress` (`History.Receiver`)
- `User.Email` from UserManager (`History.Sender`)
- `EnvelopeReceiverReadOnly.AddedWho` maps to `Receiver.EmailAddress` (principal key mapping, not receiver GUID).
#### Coordinate-Critical Fields
- Signature placement is persisted in `DocReceiverElement` as `X`, `Y`, `Width`, `Height`, `Page`.
- These coordinates are in INCHES (not PDF points, not pixels).
#### Cross-Module Tables in Same Context
- `EGDbContext` also includes UserManager entities: `User`, `Group`, `Module`, `GroupOfUser`, `ModuleOfUser`, `UserRep`, and `ClientUser`.
- Mail dispatcher entity `EmailOut` is also registered under `#if NET` and trigger-configurable.
#### Trigger-Aware Model Notes
- Trigger names are injected from `DbTriggerParams` in appsettings and applied via `ToTable(tb => tb.HasTrigger(...))`.
- Current trigger registration is explicit (`AddTrigger<T>()` calls), not attribute/reflection driven.
- Do not remove trigger mappings casually; this project depends on DB-side automation/audit behavior.
## DevExpress ## DevExpress
**License:** Commercial (v25.2.3) **License:** Commercial (v25.2.3)
@@ -320,5 +411,4 @@ Each envelope maintains independent authentication state.
None required. All config in `appsettings.json`. None required. All config in `appsettings.json`.
**Local dev ports:** **Local dev ports:**
- API: `https://localhost:8088` - App host (UI + API): `https://localhost:5131` or `http://localhost:7192`
- WebUI: `https://localhost:5131` or `http://localhost:7192`

View File

@@ -39,7 +39,7 @@ This project contains:
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions - `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services - `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities - `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server` - `EnvelopeGenerator.API` — deprecated project (no longer part of the active solution/runtime path)
### Legacy / Do Not Touch ### Legacy / Do Not Touch
- `EnvelopeGenerator.Service` - `EnvelopeGenerator.Service`
@@ -121,7 +121,7 @@ Current controller set includes:
- `SignatureController` - `SignatureController`
- `TfaRegistrationController` - `TfaRegistrationController`
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly. Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
--- ---

View File

@@ -0,0 +1,384 @@
# Database Structure Context
## Why this file exists
This is the deep-dive DB reference for migration work when schema can change and old DB knowledge may be lost.
- Scope: `EnvelopeGenerator.Domain` entities + `EnvelopeGenerator.Infrastructure/EGDbContext.cs` + live DB metadata snapshot from `DD_ECM`.
- Goal: make schema transitions predictable (what exists, how it is linked, where logic executes).
- Safety: all DB checks used read-only `SELECT` metadata queries; no data mutation.
## Snapshot metadata
- Source DB: `DD_ECM` on `SDD-VHP04-SQL19\DD_TESTING01`
- Snapshot date: `2026-10-20`
- App context: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
## Fast navigation
- High-level aggregate map: `AGENTS.md` -> `Database` section
- EF model config source: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
- Stored procedure wrappers:
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
- `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
- Direct SQL execution in controller:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
## Core data model (ER-level)
- `Envelope` (`TBSIG_ENVELOPE`) is root aggregate.
- `Envelope (1) -> (N) Document` via `TBSIG_ENVELOPE_DOCUMENT.ENVELOPE_ID`.
- `Envelope (1) -> (N) History` via `TBSIG_ENVELOPE_HISTORY.ENVELOPE_ID`.
- `Envelope (N) <-> (N) Receiver` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{ENVELOPE_ID, RECEIVER_ID}`).
- `Document (1) -> (N) DocReceiverElement` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT.DOCUMENT_ID`.
- `DocReceiverElement (1) -> (N) ElementAnnotation` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION.ELEMENT_ID`.
- `DocumentStatus` links `Envelope + Receiver` state in `TBSIG_DOCUMENT_STATUS`.
## Entity <-> table mapping (code-level)
| Entity | Table | PK in code | PK in DB | Important relation notes |
|---|---|---|---|---|
| `Envelope` | `TBSIG_ENVELOPE` | `Id` | `GUID` | Has `Documents`, `Histories`, `EnvelopeReceivers`; unique index on `Uuid` configured in EF |
| `EnvelopeReceiver` | `TBSIG_ENVELOPE_RECEIVER` | `{EnvelopeId, ReceiverId}` | `{ENVELOPE_ID, RECEIVER_ID}` | DB also has `GUID` identity (not modeled as entity PK) |
| `Document` | `TBSIG_ENVELOPE_DOCUMENT` | `Id` | `GUID` | FK `ENVELOPE_ID` |
| `DocReceiverElement` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT` | `Id` | `GUID` | Coordinates persisted here (`POSITION_X/Y`, `WIDTH`, `HEIGHT`, `PAGE`) |
| `ElementAnnotation` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION` | `Id` | `GUID` | FK `ELEMENT_ID` |
| `DocumentStatus` | `TBSIG_DOCUMENT_STATUS` | `Id` | `GUID` | FK `ENVELOPE_ID`, `RECEIVER_ID` |
| `History` | `TBSIG_ENVELOPE_HISTORY` | `Id` | `GUID` | `USER_REFERENCE` maps to `Receiver.EmailAddress` and `User.Email` when `IsMigration == false` |
| `Receiver` | `TBSIG_RECEIVER` | `Id` | `GUID` | Unique indexes in code: `Signature`, `EmailAddress` |
| `EnvelopeReceiverReadOnly` | `TBSIG_ENVELOPE_RECEIVER_READ_ONLY` | `Id` | `GUID` | `AddedWho` is FK-like mapping to `Receiver.EmailAddress` |
| `EnvelopeType` | `TBSIG_ENVELOPE_TYPE` | `Id` | `GUID` | Used as optional envelope defaults/template |
| `EmailTemplate` | `TBSIG_EMAIL_TEMPLATE` | `Id` | `GUID` | Trigger-managed `CHANGED_WHEN` |
| `EmailOut` (legacy/bridge) | `TBEMLP_EMAIL_OUT` | `Id` | `GUID` | Internal/obsolete domain class, still registered for DB access |
## Physical schema snapshot (table/column level)
Notes:
- Format per column: `COLUMN_NAME TYPE NULLABLE`.
- This section reflects live DB metadata at snapshot time.
### `TBSIG_ENVELOPE`
- `GUID int NOT NULL`
- `USER_ID int NOT NULL`
- `STATUS int NOT NULL`
- `ENVELOPE_UUID nvarchar(36) NOT NULL`
- `MESSAGE nvarchar(max) NOT NULL`
- `EXPIRES_WHEN datetime NULL`
- `EXPIRES_WARNING_WHEN datetime NULL`
- `VERIFICATION_TYPE tinyint NOT NULL`
- `TITLE nvarchar(128) NULL`
- `CONTRACT_TYPE int NULL`
- `LANGUAGE nvarchar(5) NULL`
- `SEND_REMINDER_EMAILS bit NULL`
- `FIRST_REMINDER_DAYS int NULL`
- `REMINDER_INTERVAL_DAYS int NULL`
- `ENVELOPE_TYPE int NULL`
- `CERTIFICATION_TYPE int NULL`
- `USE_ACCESS_CODE bit NULL`
- `FINAL_EMAIL_TO_CREATOR int NULL`
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
- `EXPIRES_WHEN_DAYS int NULL`
- `EXPIRES_WARNING_WHEN_DAYS int NULL`
- `TFA_ENABLED bit NOT NULL`
- `DOC_RESULT varbinary(max) NULL`
- `LANG_CODE varchar(5) NOT NULL`
- `SOURCE varchar(10) NOT NULL`
- `COMMENT varchar(128) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
### `TBSIG_ENVELOPE_RECEIVER`
- `ENVELOPE_ID int NOT NULL`
- `RECEIVER_ID int NOT NULL`
- `SEQUENCE int NOT NULL`
- `NAME nvarchar(128) NULL`
- `JOB_TITLE nvarchar(128) NULL`
- `COMPANY_NAME nvarchar(128) NULL`
- `PRIVATE_MESSAGE nvarchar(max) NULL`
- `ACCESS_CODE nvarchar(64) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `GUID bigint NOT NULL`
- `PHONE_NUMBER varchar(20) NULL`
- `PARENT_ENVELOPE_RECEIVER_ID int NOT NULL`
### `TBSIG_ENVELOPE_DOCUMENT`
- `GUID int NOT NULL`
- `ENVELOPE_ID int NOT NULL`
- `FILENAME nvarchar(256) NOT NULL`
- `FILEPATH nvarchar(256) NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `FILENAME_ORIGINAL nvarchar(256) NULL`
- `BYTE_DATA varbinary(max) NULL`
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT`
- `GUID int NOT NULL`
- `DOCUMENT_ID int NOT NULL`
- `RECEIVER_ID int NOT NULL`
- `ELEMENT_TYPE int NOT NULL`
- `POSITION_X float NOT NULL`
- `POSITION_Y float NOT NULL`
- `WIDTH float NOT NULL`
- `HEIGHT float NOT NULL`
- `PAGE int NOT NULL`
- `REQUIRED bit NOT NULL`
- `TOOLTIP nvarchar(512) NULL`
- `READ_ONLY bit NOT NULL`
- `ANNOTATION_INDEX int NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `CHANGED_WHO nvarchar(100) NULL`
- `FULL_NAME nvarchar(100) NULL`
- `POSITION nvarchar(100) NULL`
- `PLACE nvarchar(100) NULL`
- `INK varbinary(max) NULL`
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`
- `GUID bigint NOT NULL`
- `ELEMENT_ID int NOT NULL`
- `NAME nvarchar(100) NOT NULL`
- `VALUE nvarchar(max) NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `CHANGED_WHO nvarchar(100) NULL`
- `TYPE nvarchar(50) NOT NULL`
- `POSITION_X float NULL`
- `POSITION_Y float NULL`
- `WIDTH float NULL`
- `HEIGHT float NULL`
### `TBSIG_DOCUMENT_STATUS`
- `GUID int NOT NULL`
- `ENVELOPE_ID int NOT NULL`
- `RECEIVER_ID int NOT NULL`
- `STATUS int NOT NULL`
- `STATUS_CHANGED_WHEN datetime NULL`
- `VALUE nvarchar(max) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
### `TBSIG_ENVELOPE_HISTORY`
- `GUID bigint NOT NULL`
- `ENVELOPE_ID int NOT NULL`
- `USER_REFERENCE nvarchar(128) NOT NULL`
- `STATUS int NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `ACTION_DATE datetime NULL`
- `COMMENT nvarchar(max) NULL`
### `TBSIG_RECEIVER`
- `GUID int NOT NULL`
- `EMAIL_ADDRESS nvarchar(128) NOT NULL`
- `SIGNATURE nvarchar(64) NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `TOTP_SECRET_KEY nvarchar(max) NULL`
- `TFA_REG_DEADLINE datetime NULL`
- `CULTURE nvarchar(10) NULL`
### `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`
- `GUID bigint NOT NULL`
- `ENVELOPE_ID bigint NOT NULL`
- `RECEIVER_MAIL nvarchar(250) NOT NULL`
- `DATE_VALID date NOT NULL`
- `ADDED_WHO varchar(100) NOT NULL`
- `ADDED_WHEN datetime NULL`
- `CHANGED_WHO varchar(100) NULL`
- `CHANGED_WHEN datetime NULL`
### `TBSIG_ENVELOPE_TYPE`
- `GUID int NOT NULL`
- `TITLE nvarchar(128) NOT NULL`
- `LANGUAGE nvarchar(5) NOT NULL`
- `EXPIRES_DAYS int NULL`
- `CERTIFICATION_TYPE int NULL`
- `USE_ACCESS_CODE bit NULL`
- `FINAL_EMAIL_TO_CREATOR int NULL`
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `EXPIRES_WARNING_DAYS int NULL`
- `SEND_REMINDER_EMAILS bit NULL`
- `FIRST_REMINDER_DAYS int NULL`
- `REMINDER_INTERVAL_DAYS int NULL`
- `CONTRACT_TYPE int NULL`
### `TBSIG_EMAIL_TEMPLATE`
- `GUID int NOT NULL`
- `NAME nvarchar(64) NULL`
- `BODY nvarchar(max) NULL`
- `SUBJECT nvarchar(512) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `LANG_CODE varchar(5) NOT NULL`
### `TBEMLP_EMAIL_OUT`
- `GUID int NOT NULL`
- `REMINDER_TYPE_ID int NOT NULL`
- `SENDING_PROFILE int NOT NULL`
- `REFERENCE_ID int NOT NULL`
- `REFERENCE_STRING varchar(200) NULL`
- `ENTITY_ID int NULL`
- `WF_ID int NOT NULL`
- `WF_REFERENCE varchar(200) NULL`
- `EMAIL_ADRESS varchar(1000) NOT NULL`
- `EMAIL_SUBJ varchar(500) NOT NULL`
- `EMAIL_BODY varchar(max) NOT NULL`
- `EMAIL_ATTMT1 varchar(512) NULL`
- `EMAIL_SENT datetime NULL`
- `COMMENT varchar(500) NULL`
- `ADDED_WHO varchar(50) NOT NULL`
- `ADDED_WHEN datetime NULL`
- `CHANGED_WHO varchar(50) NULL`
- `CHANGED_WHEN datetime NULL`
- `ERROR_TIMESTAMP datetime NULL`
- `ERROR_MSG varchar(900) NULL`
- `ATTACHMENT1_BYTES varbinary(max) NULL`
- `ATT1_RELATED_ID bigint NULL`
- `ATT1_REL_TYPE varchar(20) NULL`
## Constraints and indexes (relevant)
From EF + DB metadata snapshot:
- `TBSIG_ENVELOPE`: PK `PK_TBSIG_ENVELOPE (GUID)`.
- `TBSIG_ENVELOPE_RECEIVER`: composite PK `PK_TBSIG_ENVELOPE_RECEIVER (ENVELOPE_ID, RECEIVER_ID)`.
- `TBSIG_DOCUMENT_RECEIVER_ELEMENT`: PK `GUID`, plus unique key `UQTBSIG_DOCUMENT_RECEIVER_ELEMENT_DocID_PosX_PosY (DOCUMENT_ID, POSITION_X, POSITION_Y)`.
- `TBSIG_RECEIVER`: PK `GUID`, unique key `UQTBSIG_RECEIVER (EMAIL_ADDRESS)`.
- `TBEMLP_EMAIL_OUT`: PK `GUID`, FK `FK_TBEMLP_EMAIL_OUT_SENDING_PROFILE`.
## Stored procedures: behavior and code call sites
### Active in .NET code path
1) `PRSIG_API_CREATE_ENVELOPE`
- Purpose: creates envelope row (`STATUS=1002`, `SOURCE='API'`, defaults) and returns `ENVELOPE_UUID` via output.
- DB writes: `TBSIG_ENVELOPE`.
- Called by:
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeExecutor.cs`
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
2) `PRSIG_API_CREATE_RECEIVER`
- Purpose: upserts `TBSIG_RECEIVER` by email, adds envelope assignment row with generated access code.
- DB writes: `TBSIG_RECEIVER`, `TBSIG_ENVELOPE_RECEIVER`.
- Called by:
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeReceiverExecutor.cs`
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
3) `PRSIG_API_ADD_DOC`
- Purpose: stores one document for envelope if absent; returns doc id.
- DB writes: `TBSIG_ENVELOPE_DOCUMENT`.
- Called by:
- SQL template `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
- executor `EnvelopeGenerator.Infrastructure/Executor/DocumentExecutor.cs`
- used in `EnvelopeReceiverController.CreateAsync`.
4) `PRSIG_API_ADD_DOC_RECEIVER_ELEM`
- Purpose: inserts signature element record if `(DOC_ID, RECEIVER_ID)` pair does not exist.
- DB writes: `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
- Called directly by:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
5) `PRSIG_API_ADD_HISTORY_STATE`
- Purpose: appends history row with status and user reference.
- DB writes: `TBSIG_ENVELOPE_HISTORY`.
- Called directly by:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
### Legacy/other path references in repo
- `PRSIG_GET_ENVELOPES_FOR_USER` and `PRSIG_GET_CLOSED_ENVELOPES_FOR_USER` are called in legacy VB path:
- `EnvelopeGenerator.CommonServices/Models/EnvelopeModel.vb`
- `PRSIG_API_WITHDRAW_ENVELOPE` exists in DB snapshot but no active `.cs` call site found in current server flow.
## Trigger behavior and where they are wired
## How trigger names are wired in app
- Config source: `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json` -> `DbTriggerParams`.
- DI binding: `EnvelopeGenerator.Infrastructure/DependencyInjection.cs` (`AddDbTriggerParams`).
- Model binding: `EnvelopeGenerator.Infrastructure/EGDbContext.cs` -> `AddTrigger<T>()` -> `ToTable(tb => tb.HasTrigger(triggerName))`.
### Trigger definitions observed in DB snapshot
1) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS`
- Fires on insert of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
- Normalizes/rewrites `POSITION_X`, `POSITION_Y` and sets `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_INS'`.
2) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD`
- Fires on update of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
- Rewrites coordinates and `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_UPD'` except when `CHANGED_WHO='OFFSET_Y_POS'`.
3) `TBSIG_ENVELOPE_HISTORY_AFT_INS`
- Fires on history insert.
- Central state machine side-effects:
- syncs `TBSIG_ENVELOPE.STATUS` for selected `100x` states,
- on `1003` applies Y-offset update to receiver elements,
- on `2005` calculates partial/fully signed and may insert follow-up `1006` history,
- on `2007` (type-dependent reject path) writes rejection emails into `TBEMLP_EMAIL_OUT` and updates envelope rejected state,
- on `3001` invitation-message counting can move envelope to `1004`.
4) `TBSIG_EMAIL_TEMPLATE_AFT_UPD`
- Fires on update of `TBSIG_EMAIL_TEMPLATE`.
- Sets `CHANGED_WHEN = GETDATE()`.
5) `TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD`
- Fires on update of `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`.
- Updates config timestamp row (`TBSIG_CONFIG.GUID = 1`).
6) `TBEMLP_EMAIL_OUT_AFT_UPD`
- Fires on update of `TBEMLP_EMAIL_OUT`.
- When `EMAIL_SENT` changes and `WF_ID in (3001..3005)`, writes corresponding history entries to `TBSIG_ENVELOPE_HISTORY`.
### Trigger mismatch alert (important)
- `DbTriggerParams` contains `TBEMLP_EMAIL_OUT_AFT_INS`, but this trigger was not found in the snapshot DB (`ExistsInDb = 0`).
- `TBEMLP_EMAIL_OUT_AFT_UPD` exists and is active.
This should be treated as environment drift and validated before migrations.
## Known model-vs-DB drift risks
These are not necessarily bugs, but they are migration risk hotspots:
- `Receiver.EmailAddress` is `[StringLength(250)]` in entity, DB column is `nvarchar(128)`.
- `EnvelopeReceiverReadOnly.AddedWho` entity expects `nvarchar(250)`, DB column is `varchar(100)`.
- `EnvelopeReceiver` entity does not model DB columns `GUID` and `PARENT_ENVELOPE_RECEIVER_ID`.
- `Envelope` entity does not explicitly model some DB columns (`VERIFICATION_TYPE`, `LANG_CODE`, `SOURCE`).
- `EmailOut` entity has `[NotMapped] AddedWhen`, while DB has `ADDED_WHEN` column.
Before schema changes, reconcile these deltas intentionally (either mapping update or documented accepted divergence).
## Migration-safe operating rules
- Do not rely on implicit trigger behavior; document expected side effects in feature code touching history/status flows.
- Any change to `TBSIG_ENVELOPE_HISTORY` semantics requires trigger impact review first.
- Keep `DbTriggerParams` synchronized with actual DB trigger inventory per environment.
- For stored procedure changes, update both:
- SQL wrapper templates (`Application/Common/SQL/*`)
- direct SQL callers in controllers/executors.
## Refresh checklist (read-only)
Run these metadata checks before/after DB migration:
1. Column inventory per table (`INFORMATION_SCHEMA.COLUMNS`).
2. Constraint inventory (`INFORMATION_SCHEMA.TABLE_CONSTRAINTS` + `KEY_COLUMN_USAGE`).
3. Trigger inventory (`sys.triggers`) and definitions (`OBJECT_DEFINITION`).
4. Stored procedure inventory (`sys.procedures`) and definitions (`OBJECT_DEFINITION`).
5. Repo call-site grep for `PRSIG_` names to detect dead or missing integration points.

View File

@@ -0,0 +1,40 @@
using DigitalData.Core.Exceptions;
using FluentValidation;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Behaviors;
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count > 0)
{
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
throw new BadRequestException(message);
}
}
return await next();
}
}

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.Application.Common.Configurations;
/// <summary>
///
/// </summary>
public class ESignParams
{
/// <summary>
///
/// </summary>
public required string ApplicationName { get; set; }
/// <summary>
///
/// </summary>
public string DefaultReason { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.Application.Common.Configurations;
/// <summary>
///
/// </summary>
public class SignatureFieldParams
{
/// <summary>
///
/// </summary>
public double Width { get; init; } = 1.77;
/// <summary>
///
/// </summary>
public double Height { get; init; } = 1.96;
}

View File

@@ -1,12 +1,14 @@
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Interfaces; using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
// TODO: Add the “Reason” property and integrate with UI and the digital signature process.
/// <summary> /// <summary>
/// Data Transfer Object representing a positioned element assigned to a document receiver. /// Data Transfer Object representing a positioned element assigned to a document receiver.
/// </summary> /// </summary>
public class DocReceiverElementDto : IDocReceiverElement public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
{ {
/// <summary> /// <summary>
/// Gets or sets the unique identifier of the element. /// Gets or sets the unique identifier of the element.
@@ -18,11 +20,21 @@ public class DocReceiverElementDto : IDocReceiverElement
/// </summary> /// </summary>
public int DocumentId { get; set; } public int DocumentId { get; set; }
/// <summary>
///
/// </summary>
public virtual DocumentDto? Document { get; set; }
/// <summary> /// <summary>
/// Gets or sets the identifier of the receiver. /// Gets or sets the identifier of the receiver.
/// </summary> /// </summary>
public int ReceiverId { get; set; } public int ReceiverId { get; set; }
/// <summary>
///
/// </summary>
public virtual ReceiverDto? Receiver { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the element. /// Gets or sets the type of the element.
/// </summary> /// </summary>
@@ -122,4 +134,41 @@ public class DocReceiverElementDto : IDocReceiverElement
/// ///
/// </summary> /// </summary>
public byte[]? Ink { get; set; } public byte[]? Ink { get; set; }
/// <summary>
/// Returns the stored signature bytes as an image payload with detected MIME type.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when signature image bytes do not exist.</exception>
public (byte[] Bytes, string ContentType, string FileExtension) ToImage()
{
if (Ink is not { Length: > 0 })
throw new InvalidOperationException("Signature image (Ink) does not exist.");
if (HasPrefix(Ink, 0x89, 0x50, 0x4E, 0x47))
return (Ink, "image/png", ".png");
if (HasPrefix(Ink, 0xFF, 0xD8, 0xFF))
return (Ink, "image/jpeg", ".jpg");
if (HasPrefix(Ink, 0x52, 0x49, 0x46, 0x46) &&
Ink.Length >= 12 &&
Ink[8] == 0x57 && Ink[9] == 0x45 && Ink[10] == 0x42 && Ink[11] == 0x50)
return (Ink, "image/webp", ".webp");
return (Ink, "application/octet-stream", ".bin");
}
private static bool HasPrefix(byte[] data, params byte[] prefix)
{
if (data.Length < prefix.Length)
return false;
for (var i = 0; i < prefix.Length; i++)
{
if (data[i] != prefix[i])
return false;
}
return true;
}
} }

View File

@@ -0,0 +1,27 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing document metadata and elements without binary payload.
/// </summary>
public record DocumentDetailsDto
{
/// <summary>
/// Gets or sets the unique identifier of the document.
/// </summary>
public int Id { get; init; }
/// <summary>
/// Gets or sets the envelope ID to which the document belongs.
/// </summary>
public int EnvelopeId { get; init; }
/// <summary>
/// Gets or sets the date and time when the document was added.
/// </summary>
public DateTime AddedWhen { get; init; }
/// <summary>
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
/// </summary>
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
}

View File

@@ -3,30 +3,25 @@
/// <summary> /// <summary>
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements. /// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
/// </summary> /// </summary>
public class DocumentDto public record DocumentDto : DocumentDetailsDto
{ {
/// <summary> /// <summary>
/// Gets or sets the unique identifier of the document. ///
/// </summary> /// </summary>
public int Id { get; set; } public DocumentDto() { }
/// <summary> /// <summary>
/// Gets or sets the envelope ID to which the document belongs. ///
/// </summary> /// </summary>
public int EnvelopeId { get; set; } /// <param name="byteData"></param>
/// <param name="details"></param>
/// <summary> public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
/// Gets or sets the date and time when the document was added. {
/// </summary> ByteData = byteData;
public DateTime AddedWhen { get; set; } }
/// <summary> /// <summary>
/// Gets or sets the binary data of the document, if available. /// Gets or sets the binary data of the document, if available.
/// </summary> /// </summary>
public byte[]? ByteData { get; set; } public byte[]? ByteData { get; set; }
/// <summary>
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
/// </summary>
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
} }

View File

@@ -62,7 +62,7 @@ public record EnvelopeDto : IEnvelope
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public int? ContractType { get; set; } public ContractType? ContractType { get; set; }
/// <summary> /// <summary>
/// Default value is 'de-DE' /// Default value is 'de-DE'
@@ -84,7 +84,42 @@ public record EnvelopeDto : IEnvelope
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public int? CertificationType { get; set; } public CertificationType? CertificationType { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToCreator { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToReceivers { get; set; }
/// <summary>
///
/// </summary>
public bool? SendReminderEmails { get; set; }
/// <summary>
///
/// </summary>
public int? FirstReminderDays { get; set; }
/// <summary>
///
/// </summary>
public int? ReminderIntervalDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWhenDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWarningWhenDays { get; set; }
/// <summary> /// <summary>
/// ///

View File

@@ -60,6 +60,11 @@ public record EnvelopeReceiverDto
/// </summary> /// </summary>
public string? PrivateMessage { get; init; } public string? PrivateMessage { get; init; }
/// <summary>
///
/// </summary>
public string? AccessCode { get; init; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>

View File

@@ -1,4 +1,6 @@
namespace EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing a type of envelope with its configuration settings. /// Data Transfer Object representing a type of envelope with its configuration settings.
@@ -28,7 +30,7 @@ public class EnvelopeTypeDto
/// <summary> /// <summary>
/// Gets or sets the certification type identifier. /// Gets or sets the certification type identifier.
/// </summary> /// </summary>
public int? CertificationType { get; set; } public CertificationType? CertificationType { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether an access code is required. /// Gets or sets a value indicating whether an access code is required.
@@ -78,5 +80,5 @@ public class EnvelopeTypeDto
/// <summary> /// <summary>
/// Gets or sets the contract type associated with the envelope type. /// Gets or sets the contract type associated with the envelope type.
/// </summary> /// </summary>
public int? ContractType { get; set; } public ContractType? ContractType { get; set; }
} }

View File

@@ -0,0 +1,16 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed class PdfPageImageDto
{
public int PageNumber { get; init; }
public double WidthPt { get; init; }
public double HeightPt { get; init; }
public int WidthPx { get; init; }
public int HeightPx { get; init; }
public string ImageDataUrl { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,6 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed class PdfRenderRequestDto
{
public byte[]? PdfBytes { get; init; }
}

View File

@@ -0,0 +1,54 @@
using EnvelopeGenerator.Application.Common.Enums;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public sealed record PdfSignOptionsDto
{
/// <summary>
///
/// </summary>
public long DocumentElementId { get; set; }
/// <summary>
///
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Reason { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Location { get; set; } = null!;
/// <summary>
///
/// </summary>
public string? Email { get; set; }
/// <summary>
///
/// </summary>
public string? Phone { get; set; }
/// <summary>
///
/// </summary>
public string ApplicationName { get; set; } = null!;
/// <summary>
///
/// </summary>
public PdfSignatureCertificationLevel CertificationLevel { get; set; }
/// <summary>
///
/// </summary>
public PdfSignatureAppearanceDto? Appearance { get; set; }
}

View File

@@ -0,0 +1,96 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public enum PdfSignatureCoordinateUnit
{
/// <summary>
///
/// </summary>
Point = 0,
/// <summary>
///
/// </summary>
Inch = 1
}
/// <summary>
///
/// </summary>
public enum PdfSignatureCoordinateOrigin
{
/// <summary>
///
/// </summary>
BottomLeft = 0,
/// <summary>
///
/// </summary>
TopLeft = 1
}
/// <summary>
///
/// </summary>
/// <param name="PageNumber"></param>
/// <param name="Left"></param>
/// <param name="Bottom"></param>
/// <param name="Width"></param>
/// <param name="Height"></param>
/// <param name="ShowLabels"></param>
/// <param name="ShowName"></param>
/// <param name="ShowDate"></param>
/// <param name="ShowReason"></param>
/// <param name="ShowLocation"></param>
/// <param name="ShowDistinguishedName"></param>
/// <param name="DateTimeFormat"></param>
/// <param name="CoordinateUnit"></param>
/// <param name="CoordinateOrigin"></param>
public sealed record PdfSignatureAppearanceDto(
int PageNumber,
double Left,
double Bottom,
double Width,
double Height,
bool ShowLabels = false,
bool ShowName = false,
bool ShowDate = false,
bool ShowReason = false,
bool ShowLocation = false,
bool ShowDistinguishedName = false,
string DateTimeFormat = "dd.MM.yyyy HH:mm:ss zzz",
PdfSignatureCoordinateUnit CoordinateUnit = PdfSignatureCoordinateUnit.Point,
PdfSignatureCoordinateOrigin CoordinateOrigin = PdfSignatureCoordinateOrigin.BottomLeft)
{
/// <summary>
///
/// </summary>
/// <param name="FamilyName"></param>
/// <param name="Size"></param>
/// <param name="IsBold"></param>
/// <param name="IsItalic"></param>
/// <param name="IsUnderline"></param>
public sealed record FontDto(string FamilyName = "Helvetica", float Size = 10, bool IsBold = false, bool IsItalic = false, bool IsUnderline = false)
{
/// <summary>
///
/// </summary>
public RgbaColorDto Color { get; init; } = new();
}
/// <summary>
///
/// </summary>
/// <param name="R"></param>
/// <param name="G"></param>
/// <param name="B"></param>
public sealed record RgbaColorDto(double R = 0, double G = 0, double B = 0);
/// <summary>
///
/// </summary>
public FontDto Font { get; init; } = new();
}

View File

@@ -0,0 +1,57 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed record PdfSignaturePrincipalDto(
string? CommonName,
string? Organization,
string? Country,
string? Full);
public sealed record PdfSignatureCertificateInfoDto(
PdfSignaturePrincipalDto IssuedTo,
PdfSignaturePrincipalDto IssuedBy,
string? SerialNumber,
DateTimeOffset? ValidFrom,
DateTimeOffset? ValidTo,
bool SelfSigned,
bool Expired,
bool NotYetValid,
bool IsSigner,
int? Version,
string? KeyAlgorithm,
string? SignatureAlgorithm,
string? Pem,
int? KeySizeBits);
public sealed record PdfSignatureCheckItemDto(
bool CoversWholeDoc,
string? Reason,
string? BusinessReason,
string? Location,
string? DeclaredName,
string? ContactInfo,
string? Email,
string? Phone,
string? DigestAlgorithm,
string? SignatureAlgorithm,
string? SignerName,
string? Issuer,
string? SerialNumber,
DateTimeOffset? ValidFrom,
DateTimeOffset? ValidTo,
string ExpiryCheckedAgainst,
IReadOnlyList<PdfSignatureCertificateInfoDto> Certificates,
bool Unchanged,
string? FieldName,
bool IsCryptographicallyValid,
string? SigningTime,
string? Filter,
string? SubFilter,
string? ApplicationName,
long? DocumentElementId,
string? OriginalDocumentSha256,
string? MessageDigestHex,
string? Error);
public sealed record PdfSignatureCheckResultDto(
bool Signed,
IReadOnlyList<PdfSignatureCheckItemDto> Signatures);

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
public class RecentReceiverSuggestionDto
{
public required string FullName { get; init; }
public required string EmailAddress { get; init; }
public required DateTime LastUsedAt { get; init; }
}

View File

@@ -0,0 +1,27 @@
namespace EnvelopeGenerator.Application.Common.Enums;
/// <summary>
///
/// </summary>
public enum PdfSignatureCertificationLevel
{
/// <summary>
/// All changes are allowed
/// </summary>
NoCertification = 0,
/// <summary>
/// No changes are allowed
/// </summary>
NoChangesAllowed,
/// <summary>
/// Allows users to fill in forms and sign the document.
/// </summary>
FillForms,
/// <summary>
/// Allows users to fill in forms, create, modify and delete annotations, and sign the document.
/// </summary>
FillFormsAndAnnotate
}

View File

@@ -0,0 +1,66 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
/// <summary>
///
/// </summary>
public interface IPdfPageImageRenderer
{
/// <summary>
///
/// </summary>
/// <param name="pdfBytes"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
/// <summary>
///
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
Stream ComposeSignaturePreviewPng(ISignatureInkPayload element);
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public bool IsSupportedImageFormat(byte[] bytes);
}
/// <summary>
///
/// </summary>
public static class PdfPageImageRendererExtensions
{
/// <summary>
///
/// </summary>
/// <param name="renderer"></param>
/// <param name="inkBytes"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public static byte[] EnsureSupportedImageBytes(this IPdfPageImageRenderer renderer, byte[] inkBytes)
{
if (!renderer.IsSupportedImageFormat(inkBytes))
throw new BadRequestException("Only PNG and JPEG signature formats are supported.");
return inkBytes;
}
public static byte[] ComposeSignaturePreviewPngBytes(this IPdfPageImageRenderer renderer, ISignatureInkPayload element)
{
using var stream = renderer.ComposeSignaturePreviewPng(element);
if (stream is MemoryStream memoryStream)
return memoryStream.ToArray();
using var copy = new MemoryStream();
stream.CopyTo(copy);
return copy.ToArray();
}
}

View File

@@ -0,0 +1,12 @@
namespace EnvelopeGenerator.Application.Common.Interfaces;
/// <summary>
///
/// </summary>
public interface IRequestContext
{
/// <summary>
///
/// </summary>
string? Origin { get; }
}

View File

@@ -0,0 +1,29 @@
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// <summary>
/// Defines PDF digital-signing operations for the application boundary.
/// </summary>
public interface IPdfDigitalSignatureService
{
/// <summary>
/// Creates a digital signature for the provided PDF bytes and returns
/// a new signed PDF document.
/// </summary>
/// <param name="inputPdf">Source PDF content stream.</param>
/// <param name="outputPdf">Destination stream for signed PDF output.</param>
/// <param name="options">Optional signing options that map to PDF signature metadata fields.</param>
/// <param name="image">Optional image stream for signature appearance.</param>
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default);
/// <summary>
/// Checks whether the provided PDF contains signature field(s) and whether
/// those signatures are cryptographically valid.
/// </summary>
/// <param name="inputPdf">PDF content stream to inspect.</param>
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
/// <returns>A structured result containing signature presence and per-signature details.</returns>
Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,98 @@
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public record DocumentReceivedNotification : EnvelopeNotificationBase
{
/// <summary>
///
/// </summary>
public required string DocumentTitle { get; init; }
/// <summary>
///
/// </summary>
public required string NameReceiver { get; init; }
/// <summary>
///
/// </summary>
public required string NameSender { get; init; }
/// <summary>
/// [SIGNATURE_TYPE]
/// </summary>
public required bool IsReadAndConfirm { private get; init; }
/// <summary>
///
/// </summary>
public string SignatureType => IsReadAndConfirm ? "Signieren" : "Lesen und bestätigen";
/// <summary>
///
/// </summary>
public required bool AdvancedSignature { private get; init; }
/// <summary>
///
/// </summary>
public string DocumentProcess => AdvancedSignature ? " und elektronisch unterschreiben" : string.Empty;
/// <summary>
///
/// </summary>
public required Uri Origin { private get; init; }
/// <summary>
///
/// </summary>
public required string EnvelopeUuid { private get; init; }
/// <summary>
///
/// </summary>
public required string ReceiverSignature { private get; init; }
private Uri EnvelopeURL => new(Origin, $"Envelope/{(EnvelopeUuid, ReceiverSignature).ToEnvelopeKey()}");
/// <summary>
///
/// </summary>
public string LinkToDocument => EnvelopeURL.AbsoluteUri;
/// <summary>
///
/// </summary>
public string LinkToDocumentText
{
get
{
var urlText = EnvelopeURL.ToString();
return $"{urlText[..Math.Min(urlText.Length, 40)]}..";
}
}
/// <summary>
///
/// </summary>
public required string Message { get; init; }
/// <summary>
///
/// </summary>
public string NamePortal { get; set; } = "signFLOW Portal";
/// <summary>
///
/// </summary>
/// <returns></returns>
public override EmailTemplateType GetTemplateType()
{
return EmailTemplateType.DocumentReceived;
}
}

View File

@@ -0,0 +1,59 @@
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public record DocumentSignedNotification : EnvelopeNotificationBase
{
/// <summary>
///
/// </summary>
public required string DocumentTitle { get; init; }
/// <summary>
///
/// </summary>
public required string NameReceiver { get; init; }
/// <summary>
///
/// </summary>
public string NamePortal { get; init; } = "signFLOW Portal";
/// <summary>
///
/// </summary>
public required bool IsReadAndConfirm { private get; init; }
/// <summary>
///
/// </summary>
public string ReceiverAction => IsReadAndConfirm ? "bestätigt" : "unterschrieben";
/// <summary>
///
/// </summary>
public string FinalStatus => IsReadAndConfirm ? "Lesebestätigung" : "Signatur";
/// <summary>
///
/// </summary>
public string FinalAction => IsReadAndConfirm ? "Empfänger bestätigt" : "Vertragspartner unterzeichnet";
/// <summary>
///
/// </summary>
public override EmailTemplateType GetTemplateType() => EmailTemplateType.DocumentSigned;
}
/*
Umschlag abgeschlossen: '[DOCUMENT_TITLE]'
---
Guten Tag [NAME_RECEIVER],<br /> <br /> Der Vorgang <B><I>'[DOCUMENT_TITLE]'</I></B> wurde erfolgreich abgeschlossen.<br /> <br /> Sie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]
*/

View File

@@ -0,0 +1,19 @@
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public abstract record EnvelopeNotificationBase
{
/// <summary>
///
/// </summary>
public abstract EmailTemplateType GetTemplateType();
/// <summary>
///
/// </summary>
public IReadOnlyList<(string FileName, byte[] Content)>? Attachments { get; init; }
}

View File

@@ -0,0 +1,18 @@
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public interface IEnvelopeNotificationService
{
/// <summary>
///
/// </summary>
/// <typeparam name="TEnvelopeNotificationBase"></typeparam>
/// <param name="receiverEmail"></param>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
where TEnvelopeNotificationBase : EnvelopeNotificationBase;
}

View File

@@ -32,6 +32,7 @@ public class MappingProfile : Profile
.ForMember(dest => dest.EnvelopeTypeTitle, .ForMember(dest => dest.EnvelopeTypeTitle,
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null)); opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
CreateMap<Document, DocumentDto>(); CreateMap<Document, DocumentDto>();
CreateMap<Document, DocumentDetailsDto>();
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>(); CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();

View File

@@ -0,0 +1,21 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
{
public required Envelope Envelope { get; init; }
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
public required string Reason { get; init; }
public required string SenderReference { get; init; }
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
}

View File

@@ -0,0 +1,42 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
{
public SendDeletedMailHandler(
IRepository<EmailTemplate> tempRepo,
IRepository<EmailOut> emailOutRepo,
IOptions<MailParams> mailParamsOptions,
IOptions<DispatcherParams> dispatcherParamsOptions)
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
{
}
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
{
emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
}
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
{
var senderName = notification.Envelope.User?.Email;
if (string.IsNullOrWhiteSpace(senderName))
senderName = notification.SenderReference;
return new Dictionary<string, string>
{
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
["[NAME_SENDER]"] = senderName ?? string.Empty,
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
["[REASON]"] = notification.Reason
};
}
}

View File

@@ -2,6 +2,7 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using MediatR; using MediatR;
@@ -96,6 +97,10 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
/// <exception cref="NotImplementedException"></exception> /// <exception cref="NotImplementedException"></exception>
public virtual async Task Handle(TNotification notification, CancellationToken cancel) public virtual async Task Handle(TNotification notification, CancellationToken cancel)
{ {
// do not send email if the envelope is deleted and the status is less than or equal to EnvelopeSaved
if (notification is EnvelopeDeletedNotification delNot && delNot.Envelope.Status <= EnvelopeStatus.EnvelopeSaved)
return;
var placeHolders = CreatePlaceHolders(notification); var placeHolders = CreatePlaceHolders(notification);
var temp = await TempRepo var temp = await TempRepo

View File

@@ -10,6 +10,8 @@ using System.Reflection;
using MediatR; using MediatR;
using EnvelopeGenerator.Application.DocReceiverElements.Commands; using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors; using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
using EnvelopeGenerator.Application.Common.Behaviors;
using FluentValidation;
namespace EnvelopeGenerator.Application; namespace EnvelopeGenerator.Application;
@@ -47,8 +49,10 @@ public static class DependencyInjection
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams))); services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
services.Configure<MailParams>(config.GetSection(nameof(MailParams))); services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
services.Configure<SignatureFieldParams>(config.GetSection(nameof(SignatureFieldParams)));
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams))); services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams))); services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams))); services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
@@ -60,6 +64,9 @@ public static class DependencyInjection
{ {
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
// Generic request validation
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
// Register SignCommand pipeline behaviors in execution order // Register SignCommand pipeline behaviors in execution order
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST) // 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>(); cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
@@ -80,6 +87,8 @@ public static class DependencyInjection
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>(); cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
}); });
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
return services; return services;
} }
} }

View File

@@ -14,6 +14,15 @@ namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
/// </summary> /// </summary>
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>> public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
{ {
/// <summary>
///
/// </summary>
public int? Id { get; set; } = null;
/// <summary>
///
/// </summary>
public bool IncludeDocument { get; set; } = false;
} }
/// <summary> /// <summary>
@@ -47,6 +56,9 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
{ {
var q = _repository.Query; var q = _repository.Query;
if (request.Id is not null)
q = q.Where(e => e.Id == request.Id);
if (request.Envelope.Id is int envelopeId) if (request.Envelope.Id is int envelopeId)
q = q.Where(e => e.Document.EnvelopeId == envelopeId); q = q.Where(e => e.Document.EnvelopeId == envelopeId);
@@ -59,6 +71,9 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
if (request.Receiver.Signature is string signature) if (request.Receiver.Signature is string signature)
q = q.Where(e => e.Receiver.Signature == signature); q = q.Where(e => e.Receiver.Signature == signature);
if( request.IncludeDocument)
q = q.Include(e => e.Document);
var elements = await q.ToListAsync(cancellationToken); var elements = await q.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements); return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);

View File

@@ -0,0 +1,102 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Documents.Queries;
/// <summary>
/// Represents a query to read document metadata/details without binary payload.
/// </summary>
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
/// <summary>
/// Handles queries for reading document details without loading BYTE_DATA.
/// </summary>
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
{
private readonly IRepository<Document> _repo;
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo)
{
_repo = repo;
}
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
{
var docQuery = _repo.Query.AsNoTracking();
if (query.UserId is int userId)
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
// shortcut: manual select avoids AutoMapper ProjectTo pulling BYTE_DATA and the full navigation
// graph (Envelope → History → User etc.) which caused an extremely slow query (~87s).
// Annotations navigation is IEnumerable<T> on the entity (not ICollection<T>), so EF Core
// cannot translate it in a Select projection — we skip it entirely here; sender editor only
// needs element coordinates, not annotation payloads.
var raw = await docQuery
.Where(d => d.EnvelopeId == query.EnvelopeId)
.Select(d => new
{
d.Id,
d.EnvelopeId,
d.AddedWhen,
Elements = d.Elements == null ? null : d.Elements.Select(e => new
{
e.Id,
e.DocumentId,
e.ReceiverId,
e.ElementType,
e.X,
e.Y,
e.Width,
e.Height,
e.Page,
e.Required,
e.Tooltip,
e.ReadOnly,
e.AnnotationIndex,
e.AddedWhen,
e.ChangedWhen,
e.FullName,
e.Position,
e.Place,
}).ToList()
})
.FirstOrDefaultAsync(cancel);
if (raw is null)
return null;
return new DocumentDetailsDto
{
Id = raw.Id,
EnvelopeId = raw.EnvelopeId,
AddedWhen = raw.AddedWhen,
Elements = raw.Elements?.Select(e => new DocReceiverElementDto
{
Id = e.Id,
DocumentId = e.DocumentId,
ReceiverId = e.ReceiverId,
ElementType = e.ElementType,
X = e.X,
Y = e.Y,
Width = e.Width,
Height = e.Height,
Page = e.Page,
Required = e.Required,
Tooltip = e.Tooltip,
ReadOnly = e.ReadOnly,
AnnotationIndex = e.AnnotationIndex,
AddedWhen = e.AddedWhen,
ChangedWhen = e.ChangedWhen,
FullName = e.FullName,
Position = e.Position,
Place = e.Place,
})
};
}
}

View File

@@ -0,0 +1,13 @@
using FluentValidation;
namespace EnvelopeGenerator.Application.Documents.Queries;
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
{
public ReadDocumentDetailsQueryValidator()
{
RuleFor(x => x.EnvelopeId)
.GreaterThan(0)
.WithMessage("EnvelopeId is required.");
}
}

View File

@@ -0,0 +1,23 @@
using FluentValidation;
namespace EnvelopeGenerator.Application.Documents.Queries;
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
{
public ReadDocumentQueryValidator()
{
RuleFor(x => x)
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
.WithMessage("Id or EnvelopeId is required.");
RuleFor(x => x.Id)
.GreaterThan(0)
.When(x => x.Id.HasValue)
.WithMessage("Id must be greater than 0.");
RuleFor(x => x.EnvelopeId)
.GreaterThan(0)
.When(x => x.EnvelopeId.HasValue)
.WithMessage("EnvelopeId must be greater than 0.");
}
}

View File

@@ -4,6 +4,7 @@
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
@@ -19,6 +20,8 @@
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" /> <PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" /> <PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" /> <PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" /> <PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="MediatR" Version="12.5.0" /> <PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />

View File

@@ -2,7 +2,7 @@
using MediatR; using MediatR;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands; namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
/// <summary> /// <summary>
/// Befehl zur Erstellung eines Umschlags. /// Befehl zur Erstellung eines Umschlags.

View File

@@ -4,7 +4,7 @@ using MediatR;
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands; namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
/// <summary> /// <summary>
/// Handles the creation of an envelope along with its associated document and recipients. /// Handles the creation of an envelope along with its associated document and recipients.

View File

@@ -1,7 +1,7 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto.Receiver;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands; namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
/// <summary> /// <summary>
/// ///

View File

@@ -0,0 +1,84 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Exceptions;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
/// <summary>
///
/// </summary>
public record RemoveEnvelopeReceiverCommand : IRequest
{
/// <summary>
///
/// </summary>
public required int EnvelopeId { get; init; }
/// <summary>
///
/// </summary>
public required int ReceiverId { get; init; }
internal int? UserId { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public RemoveEnvelopeReceiverCommand Authenticated(int userId)
{
UserId = userId;
return this;
}
}
/// <summary>
///
/// </summary>
/// <param name="Repo"></param>
public class RemoveEnvelopeReceiverCommandHandler(
IRepository<EnvelopeReceiver> Repo,
IRepository<DocReceiverElement> ElementRepo,
IRepository<ElementAnnotation> AnnotRepo) : IRequestHandler<RemoveEnvelopeReceiverCommand>
{
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task Handle(RemoveEnvelopeReceiverCommand request, CancellationToken cancel)
{
var er = await Repo.Query
.Where(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId)
.Include(er => er.Envelope)
.SingleOrDefaultAsync(cancel)
?? throw new NotFoundException();
if (er.Envelope is null)
throw new InvalidOperationException($"In {nameof(RemoveEnvelopeReceiverCommandHandler)} EnvelopeReceiver.Envelope is null." +
$"This should not happen." +
$"EnvelopeId: {request.EnvelopeId}, ReceiverId: {request.ReceiverId}");
if (request.UserId is int userId && er.Envelope.UserId != userId)
throw new AuthorizationException("Cannot remove receiver from envelope that you do not own.");
if (er.Envelope.Status > EnvelopeStatus.EnvelopeSaved)
throw new ForbiddenException("Cannot remove receiver from envelope that is already processed.");
// 1. Delete all ElementAnnotations for this EnvelopeReceiver
await AnnotRepo.DeleteAsync(ann => ann.Element.Document.EnvelopeId == request.EnvelopeId && ann.Element.ReceiverId == request.ReceiverId, cancel);
// 2. Delete all DocReceiverElements for this EnvelopeReceiver
await ElementRepo.DeleteAsync(el => el.Document.EnvelopeId == request.EnvelopeId && el.ReceiverId == request.ReceiverId, cancel);
// 3. Delete the EnvelopeReceiver
await Repo.DeleteAsync(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId, cancel);
}
}

View File

@@ -16,5 +16,9 @@ public class MappingProfile : Profile
public MappingProfile() public MappingProfile()
{ {
CreateMap<Receiver, ReceiverDto>(); CreateMap<Receiver, ReceiverDto>();
CreateMap<EnvelopeReceiver, RecentReceiverSuggestionDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Receiver.EmailAddress));
} }
} }

View File

@@ -21,7 +21,24 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
/// </remarks> /// </remarks>
public record ReadEnvelopeReceiverSecretQuery public record ReadEnvelopeReceiverSecretQuery
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
IRequest<EnvelopeReceiverSecretDto?>; IRequest<EnvelopeReceiverSecretDto?>
{
/// <summary>
///
/// </summary>
public ReadEnvelopeReceiverSecretQuery() { }
/// <summary>
///
/// </summary>
/// <param name="envelopeId"></param>
/// <param name="receiverId"></param>
public ReadEnvelopeReceiverSecretQuery(int envelopeId, int receiverId)
{
Envelope.Id = envelopeId;
Receiver.Id = receiverId;
}
}
/// <summary> /// <summary>
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>. /// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.

View File

@@ -0,0 +1,98 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
/// <summary>
/// Reads recent receiver suggestions for envelope creation.
/// </summary>
/// <remarks>
/// Sender scope is assigned via chain method to avoid exposing username mutation from API query binding.
/// </remarks>
public record ReadRecentReceiverSuggestionsQuery : IRequest<IReadOnlyList<RecentReceiverSuggestionDto>>
{
/// <summary>
/// Sender username used for scoping suggestion results.
/// </summary>
internal string? Username { get; private init; }
/// <summary>
/// Desired number of results.
/// </summary>
public int? Take { get; init; } = null;
/// <summary>
/// Returns a new query instance scoped to the specified sender username.
/// </summary>
/// <param name="username">Sender username from authenticated claims.</param>
/// <returns>Scoped query instance.</returns>
public ReadRecentReceiverSuggestionsQuery ForSender(string username)
{
return this with { Username = username };
}
}
/// <summary>
/// Handles <see cref="ReadRecentReceiverSuggestionsQuery"/>.
/// </summary>
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository)
: IRequestHandler<ReadRecentReceiverSuggestionsQuery, IReadOnlyList<RecentReceiverSuggestionDto>>
{
/// <summary>
/// Retrieves distinct recent receiver suggestions grouped by receiver email.
/// </summary>
/// <param name="request">Suggestion query.</param>
/// <param name="cancel">Cancellation token.</param>
/// <returns>Ordered list of recent receiver suggestions.</returns>
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancel)
{
var query = repository.Query
.AsNoTracking()
.Where(er => er.Receiver != null);
if (request.Username is not null)
query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username);
// 1) For each email, compute the latest usage timestamp.
var latestPerEmail = query
.GroupBy(er => er.Receiver!.EmailAddress)
.Select(g => new
{
EmailAddress = g.Key,
LastUsedAt = g.Max(er => er.AddedWhen),
});
// 2) Join back to rows that match the latest timestamp per email.
var latestRows =
from er in query
join latest in latestPerEmail
on new { EmailAddress = er.Receiver!.EmailAddress, LastUsedAt = er.AddedWhen }
equals new { latest.EmailAddress, latest.LastUsedAt }
select new
{
EmailAddress = er.Receiver!.EmailAddress,
FullName = er.Name,
LastUsedAt = er.AddedWhen,
};
// 3) Tie-break: if multiple rows share the same latest timestamp for an email,
// pick a deterministic name via Max(FullName), then order by most recent usage.
var suggestions = latestRows
.GroupBy(x => new { x.EmailAddress, x.LastUsedAt })
.Select(g => new RecentReceiverSuggestionDto
{
EmailAddress = g.Key.EmailAddress,
FullName = g.Max(x => x.FullName) ?? string.Empty,
LastUsedAt = g.Key.LastUsedAt,
})
.OrderByDescending(x => x.LastUsedAt)
.ThenBy(x => x.EmailAddress);
return request.Take is int take
? await suggestions.Take(take).ToListAsync(cancel)
: await suggestions.ToListAsync(cancel);
}
}

View File

@@ -1,6 +1,5 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using MediatR; using MediatR;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>> public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
{ {
private readonly IRepository<EnvelopeType> _repository; private readonly IRepository<EnvelopeType> _repository;
private readonly IMapper _mapper;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="repository"></param> /// <param name="repository"></param>
/// <param name="mapper"></param> public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
{ {
_repository = repository; _repository = repository;
_mapper = mapper;
} }
/// <summary> /// <summary>
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken) public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
{ {
var types = await _repository.Query.ToListAsync(cancellationToken); // TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types); // Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
return await _repository.Query
.AsNoTracking()
.Select(t => new EnvelopeTypeDto
{
Id = t.Id,
Title = t.Title ?? string.Empty,
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
ExpiresDays = t.ExpiresDays,
CertificationType = t.CertificationType,
UseAccessCode = t.UseAccessCode,
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
AddedWhen = t.AddedWhen,
ChangedWhen = t.ChangedWhen,
ExpiresWarningDays = t.ExpiresWarningDays,
SendReminderEmails = t.SendReminderEmails,
FirstReminderDays = t.FirstReminderDays,
ReminderIntervalDays = t.ReminderIntervalDays,
ContractType = t.ContractType
})
.ToListAsync(cancellationToken);
} }
} }

View File

@@ -1,6 +1,6 @@
using DigitalData.Core.Abstraction.Application.Repository; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
using EnvelopeGenerator.Domain.Constants;
using MediatR; using MediatR;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@@ -13,23 +13,96 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// </summary> /// </summary>
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?> public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
{ {
/// <summary>
/// Optionale Umschlag-ID fuer Aktualisierungsvorgaenge.
/// </summary>
public int? EnvelopeId { get; init; }
/// <summary>
/// Kennung des ausgewaehlten Umschlagtyps (Vorlage).
/// </summary>
public int? EnvelopeTypeId { get; set; }
/// <summary>
/// Sprachcode fuer Umschlaginhalt und Benachrichtigungen.
/// </summary>
public string? Language { get; set; }
/// <summary>
/// Gibt an, ob der Zugriffscode fuer Empfaenger aktiviert ist.
/// </summary>
public bool? UseAccessCode { get; set; }
/// <summary>
/// Zertifizierungstyp fuer den Signaturprozess.
/// </summary>
public CertificationType? CertificationType { get; set; }
/// <summary>
/// Finale E-Mail-Einstellung fuer den Ersteller.
/// </summary>
public int? FinalEmailToCreator { get; set; }
/// <summary>
/// Finale E-Mail-Einstellung fuer die Empfaenger.
/// </summary>
public int? FinalEmailToReceivers { get; set; }
/// <summary>
/// Gibt an, ob Erinnerungs-E-Mails aktiviert sind.
/// </summary>
public bool? SendReminderEmails { get; set; }
/// <summary>
/// Anzahl der Tage bis zur ersten Erinnerung.
/// </summary>
public int? FirstReminderDays { get; set; }
/// <summary>
/// Intervall in Tagen zwischen Erinnerungs-E-Mails.
/// </summary>
public int? ReminderIntervalDays { get; set; }
/// <summary>
/// Anzahl der Tage bis zum Ablauf des Umschlags.
/// </summary>
public int? ExpiresWhenDays { get; set; }
/// <summary>
/// Anzahl der Tage vor Ablauf, ab denen gewarnt wird.
/// </summary>
public int? ExpiresWarningWhenDays { get; set; }
/// <summary> /// <summary>
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld. /// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
/// </summary> /// </summary>
[Required] public string? Title { get; set; }
public required string Title { get; set; }
/// <summary> /// <summary>
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld. /// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
/// </summary> /// </summary>
[Required] public string? Message { get; set; }
public required string Message { get; set; }
/// <summary> /// <summary>
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false. /// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
/// </summary> /// </summary>
public bool TFAEnabled { get; set; } = false; public bool TFAEnabled { get; set; } = false;
/// <summary>
/// Gibt an, ob der Umschlag nach dem Speichern sofort versendet werden soll.
/// </summary>
public bool Send { get; init; }
/// <summary>
/// Optionaler PDF-Dokumentinhalt als Base64.
/// </summary>
public CreateEnvelopeDocumentDto? Document { get; init; }
/// <summary>
/// Empfaengerliste inklusive optionaler Signaturfeld-Koordinaten.
/// </summary>
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
/// <summary> /// <summary>
/// ID des Absenders /// ID des Absenders
/// </summary> /// </summary>
@@ -56,3 +129,30 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
[NotMapped] [NotMapped]
public bool UseSQLExecutor { get; set; } = true; public bool UseSQLExecutor { get; set; } = true;
} }
/// <summary>
///
/// </summary>
/// <param name="DataAsBase64"></param>
public record CreateEnvelopeDocumentDto(string DataAsBase64);
/// <summary>
///
/// </summary>
/// <param name="X"></param>
/// <param name="Y"></param>
/// <param name="Page"></param>
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
/// <summary>
///
/// </summary>
/// <param name="EmailAddress"></param>
/// <param name="Name"></param>
/// <param name="PhoneNumber"></param>
/// <param name="Fields"></param>
public record CreateEnvelopeReceiverDto(
string EmailAddress,
string? Name,
string? PhoneNumber,
List<CreateEnvelopeFieldDto> Fields);

View File

@@ -1,36 +1,66 @@
using AutoMapper; using AutoMapper;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities; using DigitalData.Core.Exceptions;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Envelopes.Commands; namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?> /// <param name="Provider"></param>
/// <param name="Mapper"></param>
/// <param name="EnvNtfService"></param>
/// <param name="ReqCntxt"></param>
/// <param name="Logger"></param>
public class CreateEnvelopeCommandHandler(IServiceProvider Provider, IMapper Mapper, IEnvelopeNotificationService EnvNtfService, IRequestContext ReqCntxt, ILogger<CreateEnvelopeCommandHandler> Logger) : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?>
{ {
private readonly IServiceProvider _provider; private IEnvelopeExecutor Executor => Provider.GetRequiredService<IEnvelopeExecutor>();
private IEnvelopeExecutor Executor => _provider.GetRequiredService<IEnvelopeExecutor>(); private IRepository<Envelope> Repository => Provider.GetRequiredService<IRepository<Envelope>>();
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>(); private IRepository<Document> DocumentRepository => Provider.GetRequiredService<IRepository<Document>>();
private readonly IMapper _mapper; private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => Provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
/// <summary> private IRepository<EnvelopeType> EnvelopeTypeRepository => Provider.GetRequiredService<IRepository<EnvelopeType>>();
///
/// </summary> private IRepository<Receiver> ReceiverRepository => Provider.GetRequiredService<IRepository<Receiver>>();
/// <param name="provider"></param>
/// <param name="mapper"></param> private IRepository<DocReceiverElement> DocReceiverElementRepository => Provider.GetRequiredService<IRepository<DocReceiverElement>>();
public CreateEnvelopeCommandHandler(IServiceProvider provider, IMapper mapper)
{ private IRepository<EmailTemplate> EmailTemplateRepository => Provider.GetRequiredService<IRepository<EmailTemplate>>();
_provider = provider;
_mapper = mapper; private IRepository<EmailOut> EmailOutRepository => Provider.GetRequiredService<IRepository<EmailOut>>();
}
private MailParams MailParams => Provider.GetRequiredService<IOptions<MailParams>>().Value;
private DispatcherParams DispatcherParams => Provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
private SignatureFieldParams SignatureFieldParams => Provider.GetRequiredService<IOptionsMonitor<SignatureFieldParams>>().CurrentValue;
[Obsolete("Use IConfigService instead.")]
private IConfigService ConfigService => Provider.GetRequiredService<IConfigService>();
private IMediator Mediator => Provider.GetRequiredService<IMediator>();
/// <summary> /// <summary>
/// ///
@@ -40,10 +70,541 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
/// <returns></returns> /// <returns></returns>
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel) public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
{ {
var envelope = request.UseSQLExecutor var now = DateTime.Now;
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel) var title = request.Title?.Trim() ?? string.Empty;
: await Repository.CreateAsync(request, cancel); var message = request.Message?.Trim() ?? string.Empty;
var settings = await ResolveEnvelopeSettingsAsync(request, existing: null, cancel);
return _mapper.Map<EnvelopeDto>(envelope); Envelope envelope;
if (request.EnvelopeId is int envelopeId)
{
envelope = await Repository.Query
.Include(e => e.User)
.FirstOrDefaultAsync(e => e.Id == envelopeId, cancel)
?? throw new BadRequestException($"Envelope {envelopeId} not found.");
if (envelope.UserId != request.UserId)
throw new ForbiddenException();
settings = await ResolveEnvelopeSettingsAsync(request, envelope, cancel);
await Repository.UpdateAsync(e =>
{
e.Title = title;
e.Message = message;
e.TfaEnabled = request.TFAEnabled;
e.EnvelopeTypeId = settings.EnvelopeTypeId;
e.Language = settings.Language;
e.UseAccessCode = settings.UseAccessCode;
e.CertificationType = settings.CertificationType;
e.FinalEmailToCreator = settings.FinalEmailToCreator;
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
e.SendReminderEmails = settings.SendReminderEmails;
e.FirstReminderDays = settings.FirstReminderDays;
e.ReminderIntervalDays = settings.ReminderIntervalDays;
e.ExpiresWhenDays = settings.ExpiresWhenDays;
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
e.ContractType = settings.ContractType;
e.ExpiresWhen = settings.ExpiresWhen;
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
e.Status = EnvelopeStatus.EnvelopeSaved;
e.ChangedWhen = now;
}, e => e.Id == envelopeId, cancel);
envelope.Title = title;
envelope.Message = message;
envelope.TfaEnabled = request.TFAEnabled;
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
envelope.Language = settings.Language;
envelope.UseAccessCode = settings.UseAccessCode;
envelope.CertificationType = settings.CertificationType;
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
envelope.SendReminderEmails = settings.SendReminderEmails;
envelope.FirstReminderDays = settings.FirstReminderDays;
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
envelope.ContractType = settings.ContractType;
envelope.ExpiresWhen = settings.ExpiresWhen;
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
envelope.Status = EnvelopeStatus.EnvelopeSaved;
envelope.ChangedWhen = now;
}
else
{
envelope = request.UseSQLExecutor
? await Executor.CreateEnvelopeAsync(request.UserId, title, message, request.TFAEnabled, cancel)
: await Repository.CreateAsync(new Envelope
{
UserId = request.UserId,
Status = EnvelopeStatus.EnvelopeCreated,
Uuid = Guid.NewGuid().ToString(),
Title = title,
Message = message,
AddedWhen = now,
ChangedWhen = now,
Comment = string.Empty,
EnvelopeTypeId = settings.EnvelopeTypeId,
Language = settings.Language,
TfaEnabled = request.TFAEnabled,
UseAccessCode = settings.UseAccessCode,
CertificationType = settings.CertificationType,
FinalEmailToCreator = settings.FinalEmailToCreator,
FinalEmailToReceivers = settings.FinalEmailToReceivers,
SendReminderEmails = settings.SendReminderEmails,
FirstReminderDays = settings.FirstReminderDays,
ReminderIntervalDays = settings.ReminderIntervalDays,
ExpiresWhenDays = settings.ExpiresWhenDays,
ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays,
ExpiresWhen = settings.ExpiresWhen,
ExpiresWarningWhen = settings.ExpiresWarningWhen,
ContractType = settings.ContractType
}, cancel);
if (request.UseSQLExecutor)
{
await Repository.UpdateAsync(e =>
{
e.Title = title;
e.Message = message;
e.TfaEnabled = request.TFAEnabled;
e.EnvelopeTypeId = settings.EnvelopeTypeId;
e.Language = settings.Language;
e.UseAccessCode = settings.UseAccessCode;
e.CertificationType = settings.CertificationType;
e.FinalEmailToCreator = settings.FinalEmailToCreator;
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
e.SendReminderEmails = settings.SendReminderEmails;
e.FirstReminderDays = settings.FirstReminderDays;
e.ReminderIntervalDays = settings.ReminderIntervalDays;
e.ExpiresWhenDays = settings.ExpiresWhenDays;
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
e.ExpiresWhen = settings.ExpiresWhen;
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
e.ContractType = settings.ContractType;
e.Status = EnvelopeStatus.EnvelopeSaved;
e.ChangedWhen = now;
}, e => e.Id == envelope.Id, cancel);
envelope.Title = title;
envelope.Message = message;
envelope.TfaEnabled = request.TFAEnabled;
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
envelope.Language = settings.Language;
envelope.UseAccessCode = settings.UseAccessCode;
envelope.CertificationType = settings.CertificationType;
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
envelope.SendReminderEmails = settings.SendReminderEmails;
envelope.FirstReminderDays = settings.FirstReminderDays;
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
envelope.ExpiresWhen = settings.ExpiresWhen;
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
envelope.ContractType = settings.ContractType;
envelope.Status = EnvelopeStatus.EnvelopeSaved;
envelope.ChangedWhen = now;
}
await Mediator.Send(new CreateHistoryCommand
{
EnvelopeId = envelope.Id,
UserReference = await ResolveSenderReferenceAsync(envelope, cancel),
Status = EnvelopeStatus.EnvelopeCreated
}, cancel);
}
await UpsertDocumentAsync(envelope.Id, request.Document, now, cancel);
await UpsertReceiversAndFieldsAsync(envelope.Id, request.Receivers, now, cancel);
if (request.Send)
await SendEnvelopeAsync(envelope, cancel);
envelope = await Repository.Query
.Include(e => e.User)
.Include(e => e.EnvelopeReceivers)
.ThenInclude(er => er.Receiver)
.FirstAsync(e => e.Id == envelope.Id, cancel);
return Mapper.Map<EnvelopeDto>(envelope);
}
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
{
EnvelopeType? selectedType = null;
if (request.EnvelopeTypeId is int envelopeTypeId && envelopeTypeId > 0)
{
selectedType = await EnvelopeTypeRepository.Query
.FirstOrDefaultAsync(t => t.Id == envelopeTypeId, cancel)
?? throw new BadRequestException($"Envelope type {envelopeTypeId} not found.");
}
var expiresWhenDays = Math.Max(0,
request.ExpiresWhenDays
?? selectedType?.ExpiresWhenDays
?? existing?.ExpiresWhenDays
?? 0);
var expiresWarningWhenDays = Math.Max(0,
request.ExpiresWarningWhenDays
?? selectedType?.ExpiresWarningWhenDays
?? existing?.ExpiresWarningWhenDays
?? 0);
var useAccessCode = request.UseAccessCode
?? selectedType?.UseAccessCode
?? existing?.UseAccessCode
?? false;
if (request.TFAEnabled)
useAccessCode = true;
return new EnvelopeSettings(
EnvelopeTypeId: request.EnvelopeTypeId ?? existing?.EnvelopeTypeId,
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
UseAccessCode: useAccessCode,
CertificationType: request.CertificationType
?? selectedType?.CertificationType
?? existing?.CertificationType
?? CertificationType.AdvancedElectronicSignature,
FinalEmailToCreator: request.FinalEmailToCreator
?? (int?)selectedType?.FinalEmailToCreator
?? existing?.FinalEmailToCreator
?? (int)FinalEmailType.No,
FinalEmailToReceivers: request.FinalEmailToReceivers
?? (int?)selectedType?.FinalEmailToReceivers
?? existing?.FinalEmailToReceivers
?? (int)FinalEmailType.No,
SendReminderEmails: request.SendReminderEmails
?? selectedType?.SendReminderEmails
?? existing?.SendReminderEmails
?? false,
FirstReminderDays: Math.Max(0,
request.FirstReminderDays
?? selectedType?.FirstReminderDays
?? existing?.FirstReminderDays
?? 0),
ReminderIntervalDays: Math.Max(0,
request.ReminderIntervalDays
?? selectedType?.ReminderIntervalDays
?? existing?.ReminderIntervalDays
?? 0),
ExpiresWhenDays: expiresWhenDays,
ExpiresWarningWhenDays: expiresWarningWhenDays,
ExpiresWhen: expiresWhenDays > 0 ? DateTime.Now.AddDays(expiresWhenDays) : null,
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
ContractType: existing?.ContractType
?? selectedType?.ContractType
?? ContractType.Contract);
}
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
{
if (!string.IsNullOrWhiteSpace(preferred))
return preferred.Trim();
if (!string.IsNullOrWhiteSpace(fallback1))
return fallback1.Trim();
if (!string.IsNullOrWhiteSpace(fallback2))
return fallback2.Trim();
return "de-DE";
}
private sealed record EnvelopeSettings(
int? EnvelopeTypeId,
string Language,
bool UseAccessCode,
CertificationType CertificationType,
int FinalEmailToCreator,
int FinalEmailToReceivers,
bool SendReminderEmails,
int FirstReminderDays,
int ReminderIntervalDays,
int ExpiresWhenDays,
int ExpiresWarningWhenDays,
DateTime? ExpiresWhen,
DateTime? ExpiresWarningWhen,
ContractType ContractType);
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
{
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
return envelope.User.Email;
var senderEmail = await Repository.Query
.Where(e => e.Id == envelope.Id)
.Select(e => e.User.Email)
.FirstOrDefaultAsync(cancel);
if (string.IsNullOrWhiteSpace(senderEmail))
throw new BadRequestException("Envelope sender email is missing.");
return senderEmail;
}
private async Task UpsertDocumentAsync(int envelopeId, CreateEnvelopeDocumentDto? document, DateTime now, CancellationToken cancel)
{
if (document is null || string.IsNullOrWhiteSpace(document.DataAsBase64))
return;
byte[] bytes;
try
{
bytes = Convert.FromBase64String(document.DataAsBase64);
}
catch (FormatException)
{
throw new BadRequestException("Document payload is not valid base64.");
}
var existingDocumentId = await DocumentRepository.Query
.Where(d => d.EnvelopeId == envelopeId)
.Select(d => (int?)d.Id)
.FirstOrDefaultAsync(cancel);
if (existingDocumentId is null)
{
await DocumentRepository.CreateAsync(new Document
{
EnvelopeId = envelopeId,
AddedWhen = now,
ByteData = bytes,
Filename = string.Empty,
Filepath = string.Empty,
FileNameOriginal = string.Empty
}, cancel);
return;
}
await DocumentRepository.UpdateAsync(d =>
{
d.ByteData = bytes;
}, d => d.Id == existingDocumentId.Value, cancel);
}
private async Task UpsertReceiversAndFieldsAsync(int envelopeId, IReadOnlyCollection<CreateEnvelopeReceiverDto> receivers, DateTime now, CancellationToken cancel)
{
var filteredReceivers = receivers
.Where(r => !string.IsNullOrWhiteSpace(r.EmailAddress))
.Select(r => r with
{
EmailAddress = r.EmailAddress.Trim().ToLowerInvariant(),
Name = r.Name?.Trim(),
PhoneNumber = string.IsNullOrWhiteSpace(r.PhoneNumber) ? null : r.PhoneNumber.Trim(),
Fields = r.Fields ?? new List<CreateEnvelopeFieldDto>()
})
.GroupBy(r => r.EmailAddress, StringComparer.OrdinalIgnoreCase)
.Select(g => g.First())
.ToList();
if (filteredReceivers.Count == 0)
return;
var document = await DocumentRepository.Query
.FirstOrDefaultAsync(d => d.EnvelopeId == envelopeId, cancel);
if (document is null)
return;
var currentLinks = await EnvelopeReceiverRepository.Query
.Include(er => er.Receiver)
.Where(er => er.EnvelopeId == envelopeId)
.ToListAsync(cancel);
var linksByEmail = currentLinks
.Where(er => !string.IsNullOrWhiteSpace(er.Receiver?.EmailAddress))
.GroupBy(er => er.Receiver!.EmailAddress, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.First(), StringComparer.OrdinalIgnoreCase);
var sequence = currentLinks.Count > 0 ? currentLinks.Max(er => er.Sequence) : 0;
foreach (var receiverDraft in filteredReceivers)
{
var linkResult = await GetOrCreateEnvelopeReceiverAsync(envelopeId, receiverDraft, linksByEmail, now, sequence, cancel);
var link = linkResult.Link;
sequence = linkResult.NextSequence;
await DocReceiverElementRepository.DeleteAsync(query => query
.Where(e => e.DocumentId == document.Id)
.Where(e => e.ReceiverId == link.ReceiverId), cancel);
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
{
await DocReceiverElementRepository.CreateAsync(new DocReceiverElement
{
DocumentId = document.Id,
ReceiverId = link.ReceiverId,
ElementType = 1,
X = field.X,
Y = field.Y,
Width = SignatureFieldParams.Width,
Height = SignatureFieldParams.Height,
Page = field.Page,
Required = true,
ReadOnly = false,
AnnotationIndex = 0,
AddedWhen = now,
FullName = link.Name,
Position = string.Empty,
Place = string.Empty,
Tooltip = string.Empty
}, cancel);
}
}
}
private async Task<(EnvelopeReceiver Link, int NextSequence)> GetOrCreateEnvelopeReceiverAsync(
int envelopeId,
CreateEnvelopeReceiverDto receiverDraft,
IDictionary<string, EnvelopeReceiver> linksByEmail,
DateTime now,
int sequence,
CancellationToken cancel)
{
if (linksByEmail.TryGetValue(receiverDraft.EmailAddress, out var existingLink))
{
var nextName = string.IsNullOrWhiteSpace(receiverDraft.Name) ? existingLink.Name : receiverDraft.Name!;
var nextPhoneNumber = receiverDraft.PhoneNumber ?? existingLink.PhoneNumber;
await EnvelopeReceiverRepository.UpdateAsync(er =>
{
er.Name = nextName;
er.PhoneNumber = nextPhoneNumber;
er.ChangedWhen = now;
}, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel);
existingLink.Name = nextName;
existingLink.PhoneNumber = nextPhoneNumber;
return (existingLink, sequence);
}
var receiver = await ReceiverRepository.Query
.FirstOrDefaultAsync(r => r.EmailAddress != null && r.EmailAddress.ToLower() == receiverDraft.EmailAddress, cancel);
if (receiver is null)
{
receiver = await ReceiverRepository.CreateAsync(new Receiver
{
EmailAddress = receiverDraft.EmailAddress,
Signature = receiverDraft.EmailAddress.ToUpperInvariant().GetChecksum(),
AddedWhen = now,
TotpSecretkey = string.Empty,
TfaRegDeadline = null
}, cancel);
}
sequence++;
var link = await EnvelopeReceiverRepository.CreateAsync(new EnvelopeReceiver
{
EnvelopeId = envelopeId,
ReceiverId = receiver.Id,
Sequence = sequence,
Name = receiverDraft.Name ?? receiverDraft.EmailAddress,
JobTitle = string.Empty,
CompanyName = string.Empty,
PrivateMessage = string.Empty,
AccessCode = string.Empty,
AddedWhen = now,
ChangedWhen = now,
PhoneNumber = receiverDraft.PhoneNumber ?? string.Empty
}, cancel);
link.Receiver = receiver;
linksByEmail[receiverDraft.EmailAddress] = link;
return (link, sequence);
}
private async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancel)
{
var hasDocument = await DocumentRepository.Query
.AnyAsync(d => d.EnvelopeId == envelope.Id && d.ByteData != null && d.ByteData.Length > 0, cancel);
if (!hasDocument)
throw new BadRequestException("Cannot send envelope without a document.");
var envRcvs = await EnvelopeReceiverRepository.Query
.Include(er => er.Receiver)
.Where(er => er.EnvelopeId == envelope.Id)
.ToListAsync(cancel);
if (envRcvs.Count == 0)
throw new BadRequestException("Cannot send envelope without receivers.");
var receiverIds = envRcvs.Select(l => l.ReceiverId).Distinct().ToList();
var documentIds = await DocumentRepository.Query
.Where(d => d.EnvelopeId == envelope.Id)
.Select(d => d.Id)
.ToListAsync(cancel);
var receiverFieldCounts = await DocReceiverElementRepository.Query
.Where(e => documentIds.Contains(e.DocumentId))
.Where(e => receiverIds.Contains(e.ReceiverId))
.GroupBy(e => e.ReceiverId)
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
var receiversWithoutFields = envRcvs
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
.Select(link => link.Name)
.ToList();
if (receiversWithoutFields.Count > 0)
throw new BadRequestException("Cannot send envelope because some receivers have no signature fields.");
var changedWhen = DateTime.Now;
await Repository.UpdateAsync(e =>
{
e.Status = EnvelopeStatus.EnvelopeQueued;
e.ChangedWhen = changedWhen;
}, e => e.Id == envelope.Id, cancel);
var userReference = await ResolveSenderReferenceAsync(envelope, cancel);
await Mediator.Send(new CreateHistoryCommand
{
EnvelopeId = envelope.Id,
UserReference = userReference,
Status = EnvelopeStatus.EnvelopeQueued
}, cancel);
await SendInvitationEmailsAsync(envelope, envRcvs, cancel);
envelope.Status = EnvelopeStatus.EnvelopeQueued;
}
private async Task SendInvitationEmailsAsync(
Envelope envelope,
IReadOnlyCollection<EnvelopeReceiver> envRcvs,
CancellationToken cancel)
{
foreach (var er in envRcvs)
{
var advancedSignature = envelope.CertificationType == CertificationType.AdvancedElectronicSignature;
var origin = new Uri(ReqCntxt.Origin ?? throw new InvalidOperationException($"The request origin is missing in {nameof(CreateEnvelopeCommandHandler)}."));
try
{
await EnvNtfService.Notify(er.Receiver.EmailAddress, new DocumentReceivedNotification
{
AdvancedSignature = advancedSignature,
DocumentTitle = envelope.Title ?? string.Empty,
NameReceiver = er.Name ?? string.Empty,
EnvelopeUuid = envelope.Uuid,
IsReadAndConfirm = envelope.IsReadAndConfirm(),
Message = envelope.Message ?? string.Empty,
NameSender = $"{envelope.User.Prename} {envelope.User.Name}",
Origin = origin,
ReceiverSignature = er.Receiver.Signature,
}, cancel);
}
catch(Exception ex)
{
Logger.LogError(ex, "Failed to send invitation email for receiver {emailAddress} in envelope {envelopeId}.", er.Receiver.EmailAddress, envelope.Id);
}
}
} }
} }

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
{
[Required]
public int EnvelopeId { get; init; }
[Required]
[MinLength(1)]
public string Reason { get; init; } = string.Empty;
internal int UserId { get; private set; }
public DeleteEnvelopeCommand WithAuth(int userId)
{
UserId = userId;
return this;
}
}

View File

@@ -0,0 +1,89 @@
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
{
private readonly IRepository<Envelope> _envelopeRepository;
private readonly IMediator _mediator;
public DeleteEnvelopeCommandHandler(
IRepository<Envelope> envelopeRepository,
IMediator mediator)
{
_envelopeRepository = envelopeRepository;
_mediator = mediator;
}
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
{
var reason = request.Reason?.Trim();
if (string.IsNullOrWhiteSpace(reason))
throw new BadRequestException("Please provide a reason.");
var envelope = await _envelopeRepository.Query
.Include(e => e.User)
.Include(e => e.EnvelopeReceivers)
.ThenInclude(er => er.Receiver)
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
if (envelope.UserId != request.UserId)
throw new ForbiddenException();
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
? EnvelopeStatus.EnvelopeWithdrawn
: EnvelopeStatus.EnvelopeDeleted;
var changedWhen = DateTime.Now;
envelope.Status = targetStatus;
envelope.Comment = reason;
envelope.ChangedWhen = changedWhen;
await _envelopeRepository.UpdateAsync(e =>
{
e.Status = targetStatus;
e.Comment = reason;
e.ChangedWhen = changedWhen;
}, e => e.Id == envelope.Id, cancel);
var userReference = envelope.User?.Email;
if (string.IsNullOrWhiteSpace(userReference))
throw new BadRequestException("Envelope sender email is missing.");
await _mediator.Send(new CreateHistoryCommand
{
EnvelopeId = envelope.Id,
UserReference = userReference,
Status = targetStatus,
Comment = reason
}, cancel);
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
{
if (envelopeReceiver.Receiver is null)
continue;
await _mediator.Publish(new EnvelopeDeletedNotification
{
Envelope = envelope,
EnvelopeReceiver = envelopeReceiver,
Reason = reason,
SenderReference = userReference
}, cancel);
}
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
}
}

View File

@@ -1,5 +1,5 @@
using AutoMapper; using AutoMapper;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands; using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
using EnvelopeGenerator.Application.Envelopes.Commands; using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;

View File

@@ -2,6 +2,9 @@
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.Query; using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using AutoMapper; using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
@@ -14,6 +17,11 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// </summary> /// </summary>
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>> public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
{ {
/// <summary>
///
/// </summary>
public bool IncludeDocAndDocRes { get; init; } = true;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -148,8 +156,90 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
if (request is { OnlyCompleted: true }) if (request is { OnlyCompleted: true })
query = query.Where(e => Status.Completed.Contains(e.Status)); query = query.Where(e => Status.Completed.Contains(e.Status));
if (!request.IncludeDocAndDocRes)
{
return await query
.Select(e => new EnvelopeDto
{
Id = e.Id,
UserId = e.UserId,
Status = e.Status,
StatusName = string.Empty,
Uuid = e.Uuid,
Message = e.Message,
AddedWhen = e.AddedWhen,
ChangedWhen = e.ChangedWhen,
Title = e.Title ?? string.Empty,
ContractType = e.ContractType,
Language = e.Language,
EnvelopeTypeId = e.EnvelopeTypeId,
CertificationType = e.CertificationType,
FinalEmailToCreator = e.FinalEmailToCreator,
FinalEmailToReceivers = e.FinalEmailToReceivers,
SendReminderEmails = e.SendReminderEmails,
FirstReminderDays = e.FirstReminderDays,
ReminderIntervalDays = e.ReminderIntervalDays,
ExpiresWhenDays = e.ExpiresWhenDays,
ExpiresWarningWhenDays = e.ExpiresWarningWhenDays,
UseAccessCode = e.UseAccessCode ?? true,
TFAEnabled = e.TfaEnabled ?? false,
EnvelopeTypeTitle = e.Type != null ? e.Type.Title : null,
IsAlreadySent = false,
DocResult = null,
Documents = null,
EnvelopeReceivers = e.EnvelopeReceivers.Select(er => new EnvelopeReceiverDto
{
EnvelopeId = er.EnvelopeId,
ReceiverId = er.ReceiverId,
Sequence = er.Sequence,
Name = er.Name,
JobTitle = er.JobTitle,
CompanyName = er.CompanyName,
PrivateMessage = er.PrivateMessage,
AccessCode = er.AccessCode,
AddedWhen = er.AddedWhen,
ChangedWhen = er.ChangedWhen,
HasPhoneNumber = er.HasPhoneNumber,
Receiver = er.Receiver == null
? null
: new ReceiverDto
{
Id = er.Receiver.Id,
EmailAddress = er.Receiver.EmailAddress,
Signature = er.Receiver.Signature,
AddedWhen = er.Receiver.AddedWhen,
TfaRegDeadline = er.Receiver.TfaRegDeadline
}
}),
Histories = e.Histories.Select(h => new HistoryDto
{
Id = h.Id,
EnvelopeId = h.EnvelopeId,
UserReference = h.UserReference,
Status = h.Status,
StatusName = null,
AddedWhen = h.AddedWhen,
ActionDate = h.ActionDate,
Comment = h.Comment,
Receiver = h.Receiver == null
? null
: new ReceiverDto
{
Id = h.Receiver.Id,
EmailAddress = h.Receiver.EmailAddress,
Signature = h.Receiver.Signature,
AddedWhen = h.Receiver.AddedWhen,
TfaRegDeadline = h.Receiver.TfaRegDeadline
}
})
})
.ToListAsync(cancel);
}
var envelopes = await query var envelopes = await query
.Include(e => e.Type)
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver) .Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
.Include(e => e.Histories)
.ToListAsync(cancel); .ToListAsync(cancel);
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes); return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);

View File

@@ -0,0 +1,62 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// <summary>
///
/// </summary>
public record ReadSignedEnvelopeQuery : IRequest<IEnumerable<EnvelopeDto>>
{
/// <summary>
///
/// </summary>
public bool? Burned { get; init; } = null;
internal int? UserId { get; private set; } = null;
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public ReadSignedEnvelopeQuery Authorized(int userId)
{
UserId = userId;
return this;
}
}
/// <summary>
///
/// </summary>
/// <param name="Repo"></param>
/// <param name="Mapper"></param>
public class ReadSignedEnvelopeQueryHandler(IRepository<Envelope> Repo, IMapper Mapper) : IRequestHandler<ReadSignedEnvelopeQuery, IEnumerable<EnvelopeDto>>
{
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<IEnumerable<EnvelopeDto>> Handle(ReadSignedEnvelopeQuery request, CancellationToken cancel)
{
var eQuery = Repo.Query.Where(e => e.Status == Domain.Constants.EnvelopeStatus.EnvelopeCompletelySigned);
if (request.Burned is bool burned)
eQuery = burned ? eQuery.Where(e => e.DocResult != null) : eQuery.Where(e => e.DocResult == null);
if(request.UserId is int userId)
eQuery = eQuery.Where(e => e.UserId == userId);
var envelopes = await eQuery.ToListAsync(cancel);
return Mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
}
}

View File

@@ -0,0 +1,109 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.PdfSigning;
/// <summary>
///
/// </summary>
public record FinalizeEnvelopeCommand : EnvelopeQueryBase, IRequest
{
internal int? UserId { get; private set; } = null;
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public FinalizeEnvelopeCommand Authorized(int userId)
{
UserId = userId;
return this;
}
/// <summary>
///
/// </summary>
public bool BurnedAllowed { get; init; } = false;
/// <summary>
///
/// </summary>
public bool SaveDocResult { get; init; } = true;
}
/// <summary>
///
/// </summary>
/// <param name="Repo"></param>
/// <param name="Sender"></param>
/// <param name="NtfServer"></param>
public class FinalizeEnvelopeCommandHandler(IRepository<Envelope> Repo, ISender Sender, IEnvelopeNotificationService NtfServer) : IRequestHandler<FinalizeEnvelopeCommand>
{
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task Handle(FinalizeEnvelopeCommand request, CancellationToken cancel)
{
var envelopeQuery = Repo.Query;
if (request.Id is int id)
envelopeQuery = envelopeQuery.Where(e => e.Id == id);
else if (request.Uuid is string uuid)
envelopeQuery = envelopeQuery.Where(e => e.Uuid == uuid);
var envelope = await envelopeQuery
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
.Include(e => e.Documents)
.SingleOrDefaultAsync(cancel)
?? throw new NotFoundException($"Envelope is not found ID: {request.Id} UUID: {request.Uuid}");
if(request.UserId is int userId && userId != envelope.UserId)
throw new InvalidOperationException($"Envelope is not owned by user ID: {request.Id} UUID: {request.Uuid}");
// already burned
if (!request.BurnedAllowed && envelope.IsBurned())
throw new InvalidOperationException($"Envelope is already burned ID: {request.Id} UUID: {request.Uuid}");
// envelope is not completely signed
if (envelope.Status!= EnvelopeStatus.EnvelopeCompletelySigned)
throw new InvalidOperationException($"Envelope status is not completely signed ID: {request.Id} UUID: {request.Uuid}");
using var docStream = await Sender.Send(new SignPdfByEnvelopeCommand()
{
Envelope = envelope
}, cancel);
if (docStream.CanSeek)
docStream.Position = 0;
using var ms = new MemoryStream();
docStream.CopyTo(ms);
byte[] doc = ms.ToArray();
foreach (var er in envelope.EnvelopeReceivers)
{
await NtfServer.Notify(er.Receiver.EmailAddress, new DocumentSignedNotification()
{
DocumentTitle = envelope.Title,
NameReceiver = er.Name,
Attachments = [($"unterzeichnetes-dokument.pdf", doc)],
IsReadAndConfirm = envelope.IsReadAndConfirm(),
}, cancel);
}
if (request.SaveDocResult)
await Repo.UpdateAsync(e => e.DocResult = doc, e => e.Id == envelope.Id, cancel);
}
}

View File

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

View File

@@ -0,0 +1,18 @@
using FluentValidation;
namespace EnvelopeGenerator.Application.PdfSigning;
public class SignPdfByElementCommandValidator : AbstractValidator<SignPdfByElementCommand>
{
public SignPdfByElementCommandValidator()
{
RuleFor(x => x)
.Must(x => (x.Element is not null) ^ x.ElementId.HasValue)
.WithMessage("Exactly one of Element or ElementId must be provided.");
RuleFor(x => x.ElementId)
.GreaterThan(0)
.When(x => x.ElementId.HasValue)
.WithMessage("ElementId must be greater than 0.");
}
}

View File

@@ -0,0 +1,94 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Exceptions;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.PdfSigning;
/// <summary>
///
/// </summary>
public record SignPdfByEnvelopeCommand : EnvelopeQueryBase, IRequest<Stream>
{
/// <summary>
///
/// </summary>
public int? ReceiverId { get; init; } = null;
internal int? UserId { get; private set; } = null;
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public SignPdfByEnvelopeCommand Authorized(int userId)
{
UserId = userId;
return this;
}
internal Envelope? Envelope { get; set; }
}
/// <summary>
///
/// </summary>
public class SignPdfByEnvelopeCommandHandler(IRepository<Envelope> Repo, ISender sender) : IRequestHandler<SignPdfByEnvelopeCommand, Stream>
{
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<Stream> Handle(SignPdfByEnvelopeCommand request, CancellationToken cancel)
{
if (request.Envelope is null)
{
var envelopeQuery = Repo.Query;
if (request.Id is int id)
envelopeQuery = envelopeQuery.Where(e => e.Id == id);
else if (request.Uuid is string uuid)
envelopeQuery = envelopeQuery.Where(e => e.Uuid == uuid);
request.Envelope = await envelopeQuery.Include(e => e.EnvelopeReceivers).Include(e => e.Documents).SingleOrDefaultAsync(cancel)
?? throw new NotFoundException($"Envelope is not found ID: {request.Id} UUID: {request.Uuid}");
}
var envelope = request.Envelope;
if (request.UserId is int userId && userId != envelope.UserId)
throw new AuthorizationException($"Envelope is not authorized for user with Id: {request.UserId} Envelope ID: {request.Id} UUID: {request.Uuid}");
if (envelope.EnvelopeReceivers.Count == 0)
throw new NotFoundException($"Envelope has no receivers to sign ID: {request.Id} UUID: {request.Uuid}");
if(envelope.Documents.FirstOrDefault() is not Document document || document.ByteData is not byte[] byteData || byteData.Length == 0)
throw new NotFoundException($"Envelope has no documents to sign ID: {request.Id} UUID: {request.Uuid}");
Stream doc = new MemoryStream(byteData);
var eReceivers = request.ReceiverId is int receiverId
? envelope.EnvelopeReceivers.Where(e => e.ReceiverId == receiverId)
: envelope.EnvelopeReceivers;
foreach (var eReceiver in eReceivers)
{
var signedDoc = await sender.Send(new SignPdfByEnvelopeReceiverCommand()
{
EnvelopeId = eReceiver.EnvelopeId,
ReceiverId = eReceiver.ReceiverId
}, cancel);
await doc.DisposeAsync();
doc = signedDoc;
}
return doc;
}
}

View File

@@ -0,0 +1,99 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Enums;
using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Envelopes.Queries;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Exceptions;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.PdfSigning;
/// <summary>
///
/// </summary>
public record SignPdfByEnvelopeReceiverCommand : IRequest<Stream>
{
/// <summary>
///
/// </summary>
public int EnvelopeId { get; init; }
/// <summary>
///
/// </summary>
public int ReceiverId { get; init; }
/// <summary>
///
/// </summary>
public Stream? Document { get; set; }
}
/// <summary>
///
/// </summary>
public class SignPdfByEnvelopeReceiverCommandHandler(IRepository<DocReceiverElement> Repo, IMapper Mapper, ISender Sender) : IRequestHandler<SignPdfByEnvelopeReceiverCommand, Stream>
{
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task<Stream> Handle(SignPdfByEnvelopeReceiverCommand request, CancellationToken cancel)
{
cancel.ThrowIfCancellationRequested();
// read elements
var elements = await Repo.Where(el => el.ReceiverId == request.ReceiverId && el.Document.EnvelopeId == request.EnvelopeId)
.Include(e => e.Receiver)
.ToListAsync(cancel) ?? throw new NotFoundException($"No elements found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}.");
var elementDtos = Mapper.Map<List<DocReceiverElementDto>>(elements)
.OrderBy(e => e.Page)
.ThenBy(e => e.Id)
.ToList();
// read secrets for phone number
var erSecrets = await Sender.Send(new ReadEnvelopeReceiverSecretQuery()
{
Envelope = new() { Id = request.EnvelopeId },
Receiver = new() { Id = request.ReceiverId }
}, cancel) ?? throw new DataIntegrityException($"No secrets found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}.");
// read document
if(request.Document is null)
{
var unsignedDoc = await Sender.Send(new ReadDocumentQuery(EnvelopeId: request.EnvelopeId), cancel);
var unsignedDocBytes = unsignedDoc.ByteData ?? throw new NotFoundException($"Document not found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}.");
request.Document = new MemoryStream(unsignedDocBytes);
}
Stream doc = request.Document;
// sign document for each element
foreach (var element in elementDtos)
{
var lastElement = element == elementDtos.Last();
var signedDoc = await Sender.Send(new SignPdfByElementCommand
{
Document = doc,
Element = element,
CertificationLevel = lastElement ? PdfSignatureCertificationLevel.NoChangesAllowed : PdfSignatureCertificationLevel.FillFormsAndAnnotate,
Phone = erSecrets.PhoneNumber
}, cancel);
await doc.DisposeAsync();
doc = signedDoc;
}
return doc;
}
}

View File

@@ -123,6 +123,9 @@
<data name="Back" xml:space="preserve"> <data name="Back" xml:space="preserve">
<value>Zurück</value> <value>Zurück</value>
</data> </data>
<data name="Next" xml:space="preserve">
<value>Weiter</value>
</data>
<data name="AuthenticatorSetup_Prefix" xml:space="preserve"> <data name="AuthenticatorSetup_Prefix" xml:space="preserve">
<value>Klicken Sie auf den</value> <value>Klicken Sie auf den</value>
</data> </data>
@@ -564,6 +567,12 @@
<data name="AddReceiver" xml:space="preserve"> <data name="AddReceiver" xml:space="preserve">
<value>Empfänger hinzufügen</value> <value>Empfänger hinzufügen</value>
</data> </data>
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
<value>Zuletzt verwendete Empfänger</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Überspringen</value>
</data>
<data name="DeleteReceiver" xml:space="preserve"> <data name="DeleteReceiver" xml:space="preserve">
<value>Empfänger löschen</value> <value>Empfänger löschen</value>
</data> </data>
@@ -651,4 +660,125 @@
<data name="Status" xml:space="preserve"> <data name="Status" xml:space="preserve">
<value>Status</value> <value>Status</value>
</data> </data>
<data name="ChangedAt" xml:space="preserve"><value>Geändert am</value></data>
<data name="Close" xml:space="preserve"><value>Schließen</value></data>
<data name="CloseLanguageMenu" xml:space="preserve"><value>Sprachmenü schließen</value></data>
<data name="ConfirmSignature" xml:space="preserve"><value>Unterschrift bestätigen</value></data>
<data name="Created" xml:space="preserve"><value>Erstellt</value></data>
<data name="Delete" xml:space="preserve"><value>Löschen</value></data>
<data name="Document" xml:space="preserve"><value>Dokument</value></data>
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Dokument konnte nicht geladen werden</value></data>
<data name="DocumentLoading" xml:space="preserve"><value>Dokument wird geladen...</value></data>
<data name="EmailAddress" xml:space="preserve"><value>E-Mail-Adresse</value></data>
<data name="Envelope" xml:space="preserve"><value>Umschlag</value></data>
<data name="Error" xml:space="preserve"><value>Fehler</value></data>
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Fehler beim Laden des Dokuments: {0}</value></data>
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Entwicklungsmodus</value></data>
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Dies kann dazu führen, dass vertrauliche Informationen aus Ausnahmen Endbenutzern angezeigt werden. Für lokales Debugging setzen Sie ASPNETCORE_ENVIRONMENT auf Development und starten die App neu.</value></data>
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Im Entwicklungsmodus werden detailliertere Fehlerinformationen angezeigt.</value></data>
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Der Entwicklungsmodus sollte in bereitgestellten Anwendungen nicht aktiviert sein.</value></data>
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten.</value></data>
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Anfrage-ID</value></data>
<data name="ErrorWithReason" xml:space="preserve"><value>Fehler: {0}</value></data>
<data name="From" xml:space="preserve"><value>Von</value></data>
<data name="FullName" xml:space="preserve"><value>Vor- und Nachname</value></data>
<data name="History" xml:space="preserve"><value>Historie</value></data>
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digitale Unterschrift</value></data>
<data name="Index_PdfExport" xml:space="preserve"><value>PDF-Export</value></data>
<data name="Index_SecureAccess" xml:space="preserve"><value>Sicherer Zugang</value></data>
<data name="Index_Subtitle" xml:space="preserve"><value>Willkommen im eSign-Portal</value></data>
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Zur Umschlag-Übersicht</value></data>
<data name="Languages" xml:space="preserve"><value>Sprachen</value></data>
<data name="Loading" xml:space="preserve"><value>Lädt...</value></data>
<data name="Login" xml:space="preserve"><value>Anmelden</value></data>
<data name="LoginInProgress" xml:space="preserve"><value>Anmelden ...</value></data>
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Nicht authentifiziert. Bitte melden Sie sich erneut an.</value></data>
<data name="Only" xml:space="preserve"><value>Nur</value></data>
<data name="OpenDocument" xml:space="preserve"><value>Dokument öffnen</value></data>
<data name="Optional" xml:space="preserve"><value>optional</value></data>
<data name="Pending" xml:space="preserve"><value>Ausstehend</value></data>
<data name="Preview" xml:space="preserve"><value>Vorschau</value></data>
<data name="PrivacyPolicy" xml:space="preserve"><value>Datenschutz</value></data>
<data name="Receiver" xml:space="preserve"><value>Empfänger</value></data>
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Zugangscode eingeben</value></data>
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.</value></data>
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.</value></data>
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</value></data>
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Ungültiger Zugangscode.</value></data>
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</value></data>
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Dokument nicht gefunden.</value></data>
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang mit Zugangscode</value></data>
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.</value></data>
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Möchten Sie das Dokument verbindlich unterschreiben?</value></data>
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope-Schlüssel fehlt.</value></data>
<data name="Refresh" xml:space="preserve"><value>Aktualisieren</value></data>
<data name="RequiredField" xml:space="preserve"><value>Pflichtfeld</value></data>
<data name="Selected" xml:space="preserve"><value>ausgewählte</value></data>
<data name="Send" xml:space="preserve"><value>Senden</value></data>
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Aktive Umschläge</value></data>
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Abgeschlossene Umschläge</value></data>
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Neuer Umschlag</value></data>
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Neuen Umschlag erstellen</value></data>
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Umschlag löschen</value></data>
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Begründung eingeben...</value></data>
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.</value></data>
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Bitte geben Sie einen Löschgrund an.</value></data>
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag löschen</value></data>
<data name="Sender_Edit" xml:space="preserve"><value>Bearbeiten</value></data>
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag bearbeiten</value></data>
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Aktuelle Ansicht exportieren</value></data>
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Exportformat auswählen</value></data>
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export der aktuellen Tabellenansicht</value></data>
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Fehler beim Laden der Umschläge</value></data>
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Umschläge werden geladen...</value></data>
<data name="Sender_NoHistory" xml:space="preserve"><value>Keine Historie vorhanden</value></data>
<data name="Sender_NoReceivers" xml:space="preserve"><value>Keine Empfänger</value></data>
<data name="Sender_Preview_Title" xml:space="preserve"><value>Ausgewählten Umschlag als PDF anzeigen</value></data>
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf.</value></data>
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Signatur hinzufügen</value></data>
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Klicken Sie auf die Stelle im Dokument für</value></data>
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.</value></data>
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>E-Mail-Vorschläge werden geladen...</value></data>
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Fehler beim Erstellen des Umschlags</value></data>
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Fehler beim Laden des Umschlags #{0}: {1}</value></data>
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Fehler beim Laden der Datei: {0}</value></data>
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Bitte unterzeichnen Sie das beigefügte Dokument.</value></data>
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Kein Dokument geladen</value></data>
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Keine Seiten zur Anzeige verfügbar.</value></data>
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Noch keine Empfänger hinzugefügt.</value></data>
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Seite</value></data>
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Signaturfeld platzieren</value></data>
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Alle Signaturfelder entfernen</value></data>
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Bitte wählen Sie eine PDF-Datei aus.</value></data>
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Der bestehende Entwurf wurde versendet.</value></data>
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Umschlag gesendet</value></data>
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Umschlag wurde erfolgreich gesendet.</value></data>
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signaturfeld</value></data>
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>z.B. Arbeitsvertrag 2025</value></data>
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Zur Übersicht</value></data>
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.</value></data>
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Diese E-Mail-Adresse wurde bereits hinzugefügt.</value></data>
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Bitte geben Sie eine gültige E-Mail-Adresse ein.</value></data>
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Bitte geben Sie eine E-Mail-Adresse ein.</value></data>
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Bitte geben Sie einen Vor- und Nachnamen ein.</value></data>
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Bitte fügen Sie mindestens einen Empfänger hinzu.</value></data>
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Bitte platzieren Sie mindestens ein Signaturfeld.</value></data>
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Bitte geben Sie einen Titel ein.</value></data>
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Bitte laden Sie zuerst ein PDF-Dokument hoch.</value></data>
<data name="SenderLogin_Description" xml:space="preserve"><value>Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.</value></data>
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Administrator.</value></data>
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</value></data>
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Ungültige Anmeldedaten.</value></data>
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Passwort eingeben</value></data>
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang zum Sender-Dashboard</value></data>
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender Anmeldung</value></data>
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Benutzername eingeben</value></data>
<data name="ServerError" xml:space="preserve"><value>Serverfehler.</value></data>
<data name="Signed" xml:space="preserve"><value>Unterschrieben</value></data>
<data name="SignedDocument" xml:space="preserve"><value>Signiertes Dokument</value></data>
<data name="System" xml:space="preserve"><value>System</value></data>
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</value></data>
<data name="UploadPdf" xml:space="preserve"><value>PDF hochladen</value></data>
<data name="UserName" xml:space="preserve"><value>Benutzername</value></data>
<data name="VerifyInProgress" xml:space="preserve"><value>Überprüfen ...</value></data>
</root> </root>

View File

@@ -123,6 +123,9 @@
<data name="Back" xml:space="preserve"> <data name="Back" xml:space="preserve">
<value>Back</value> <value>Back</value>
</data> </data>
<data name="Next" xml:space="preserve">
<value>Next</value>
</data>
<data name="AuthenticatorSetup_Prefix" xml:space="preserve"> <data name="AuthenticatorSetup_Prefix" xml:space="preserve">
<value>Click the</value> <value>Click the</value>
</data> </data>
@@ -564,6 +567,12 @@
<data name="AddReceiver" xml:space="preserve"> <data name="AddReceiver" xml:space="preserve">
<value>Add Receiver</value> <value>Add Receiver</value>
</data> </data>
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
<value>Recently used recipients</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Skip</value>
</data>
<data name="DeleteReceiver" xml:space="preserve"> <data name="DeleteReceiver" xml:space="preserve">
<value>Delete Receiver</value> <value>Delete Receiver</value>
</data> </data>
@@ -651,4 +660,125 @@
<data name="Status" xml:space="preserve"> <data name="Status" xml:space="preserve">
<value>Status</value> <value>Status</value>
</data> </data>
<data name="ChangedAt" xml:space="preserve"><value>Changed at</value></data>
<data name="Close" xml:space="preserve"><value>Close</value></data>
<data name="CloseLanguageMenu" xml:space="preserve"><value>Close language menu</value></data>
<data name="ConfirmSignature" xml:space="preserve"><value>Confirm signature</value></data>
<data name="Created" xml:space="preserve"><value>Created</value></data>
<data name="Delete" xml:space="preserve"><value>Delete</value></data>
<data name="Document" xml:space="preserve"><value>Document</value></data>
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Document could not be loaded</value></data>
<data name="DocumentLoading" xml:space="preserve"><value>Document is loading...</value></data>
<data name="EmailAddress" xml:space="preserve"><value>Email address</value></data>
<data name="Envelope" xml:space="preserve"><value>Envelope</value></data>
<data name="Error" xml:space="preserve"><value>Error</value></data>
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Error loading document: {0}</value></data>
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Development Mode</value></data>
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>This can result in sensitive exception information being shown to end users. For local debugging, set ASPNETCORE_ENVIRONMENT to Development and restart the app.</value></data>
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Switching to Development will display more detailed error information.</value></data>
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>The Development environment should not be enabled in deployed applications.</value></data>
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>An error occurred while processing your request.</value></data>
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Request ID</value></data>
<data name="ErrorWithReason" xml:space="preserve"><value>Error: {0}</value></data>
<data name="From" xml:space="preserve"><value>From</value></data>
<data name="FullName" xml:space="preserve"><value>First and last name</value></data>
<data name="History" xml:space="preserve"><value>History</value></data>
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digital signature</value></data>
<data name="Index_PdfExport" xml:space="preserve"><value>PDF export</value></data>
<data name="Index_SecureAccess" xml:space="preserve"><value>Secure access</value></data>
<data name="Index_Subtitle" xml:space="preserve"><value>Welcome to the eSign portal</value></data>
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Go to envelope overview</value></data>
<data name="Languages" xml:space="preserve"><value>Languages</value></data>
<data name="Loading" xml:space="preserve"><value>Loading...</value></data>
<data name="Login" xml:space="preserve"><value>Sign in</value></data>
<data name="LoginInProgress" xml:space="preserve"><value>Signing in ...</value></data>
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Not authenticated. Please sign in again.</value></data>
<data name="Only" xml:space="preserve"><value>Only</value></data>
<data name="OpenDocument" xml:space="preserve"><value>Open document</value></data>
<data name="Optional" xml:space="preserve"><value>optional</value></data>
<data name="Pending" xml:space="preserve"><value>Pending</value></data>
<data name="Preview" xml:space="preserve"><value>Preview</value></data>
<data name="PrivacyPolicy" xml:space="preserve"><value>Privacy policy</value></data>
<data name="Receiver" xml:space="preserve"><value>Receiver</value></data>
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Enter access code</value></data>
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Please enter the access code you received by email to securely open the document.</value></data>
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the document sender.</value></data>
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>The entered code is incorrect. Please try again.</value></data>
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Invalid access code.</value></data>
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>The provided access code could not be matched to a document. Please verify the link in your email.</value></data>
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document not found.</value></data>
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Secure access with access code</value></data>
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>This action cannot be undone. By confirming, you declare that you have electronically signed the document shown above. The signed document will then be delivered to all involved parties.</value></data>
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Do you want to sign this document as binding?</value></data>
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope key is missing.</value></data>
<data name="Refresh" xml:space="preserve"><value>Refresh</value></data>
<data name="RequiredField" xml:space="preserve"><value>Required field</value></data>
<data name="Selected" xml:space="preserve"><value>selected</value></data>
<data name="Send" xml:space="preserve"><value>Send</value></data>
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Active envelopes</value></data>
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Completed envelopes</value></data>
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>New envelope</value></data>
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Create new envelope</value></data>
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Delete envelope</value></data>
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Enter reason...</value></data>
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Please provide a reason why the envelope should be deleted/withdrawn.</value></data>
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Please provide a deletion reason.</value></data>
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Delete selected envelope</value></data>
<data name="Sender_Edit" xml:space="preserve"><value>Edit</value></data>
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Edit selected envelope</value></data>
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Export current view</value></data>
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Select export format</value></data>
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export current table view</value></data>
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Error loading envelopes</value></data>
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Loading envelopes...</value></data>
<data name="Sender_NoHistory" xml:space="preserve"><value>No history available</value></data>
<data name="Sender_NoReceivers" xml:space="preserve"><value>No receivers</value></data>
<data name="Sender_Preview_Title" xml:space="preserve"><value>Show selected envelope as PDF</value></data>
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>The document for envelope #{0} was not found or you do not have access.</value></data>
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Add signature</value></data>
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Click on the location in the document for</value></data>
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Previously used email addresses are suggested while typing.</value></data>
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Loading email suggestions...</value></data>
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Error creating envelope</value></data>
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Error loading envelope #{0}: {1}</value></data>
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Error loading file: {0}</value></data>
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Please sign the attached document.</value></data>
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>No document loaded</value></data>
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>No pages to display.</value></data>
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>No receivers added yet.</value></data>
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Place signature field</value></data>
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Remove all signature fields</value></data>
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Please select a PDF file.</value></data>
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>The existing draft has been sent.</value></data>
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Envelope sent</value></data>
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Envelope was sent successfully.</value></data>
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signature field</value></data>
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>e.g. Employment contract 2025</value></data>
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>To overview</value></data>
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Upload a PDF file to place signature fields.</value></data>
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>This email address has already been added.</value></data>
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Please enter a valid email address.</value></data>
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Please enter an email address.</value></data>
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Please enter first and last name.</value></data>
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Please add at least one receiver.</value></data>
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Please place at least one signature field.</value></data>
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Please enter a title.</value></data>
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Please upload a PDF document first.</value></data>
<data name="SenderLogin_Description" xml:space="preserve"><value>Please sign in with your credentials to access the sender dashboard.</value></data>
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the administrator.</value></data>
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Username or password is incorrect. Please try again.</value></data>
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Invalid credentials.</value></data>
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Enter password</value></data>
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Secure access to sender dashboard</value></data>
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender login</value></data>
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Enter username</value></data>
<data name="ServerError" xml:space="preserve"><value>Server error.</value></data>
<data name="Signed" xml:space="preserve"><value>Signed</value></data>
<data name="SignedDocument" xml:space="preserve"><value>Signed document</value></data>
<data name="System" xml:space="preserve"><value>System</value></data>
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>An unexpected error occurred. Please try again later.</value></data>
<data name="UploadPdf" xml:space="preserve"><value>Upload PDF</value></data>
<data name="UserName" xml:space="preserve"><value>Username</value></data>
<data name="VerifyInProgress" xml:space="preserve"><value>Verifying ...</value></data>
</root> </root>

View File

@@ -123,6 +123,9 @@
<data name="Back" xml:space="preserve"> <data name="Back" xml:space="preserve">
<value>Retour</value> <value>Retour</value>
</data> </data>
<data name="Next" xml:space="preserve">
<value>Suivant</value>
</data>
<data name="AuthenticatorSetup_Prefix" xml:space="preserve"> <data name="AuthenticatorSetup_Prefix" xml:space="preserve">
<value>Cliquez sur</value> <value>Cliquez sur</value>
</data> </data>
@@ -564,6 +567,12 @@
<data name="AddReceiver" xml:space="preserve"> <data name="AddReceiver" xml:space="preserve">
<value>Ajouter un destinataire</value> <value>Ajouter un destinataire</value>
</data> </data>
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
<value>Destinataires recents</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Passer</value>
</data>
<data name="DeleteReceiver" xml:space="preserve"> <data name="DeleteReceiver" xml:space="preserve">
<value>Supprimer le destinataire</value> <value>Supprimer le destinataire</value>
</data> </data>
@@ -651,4 +660,125 @@
<data name="Status" xml:space="preserve"> <data name="Status" xml:space="preserve">
<value>Statut</value> <value>Statut</value>
</data> </data>
<data name="ChangedAt" xml:space="preserve"><value>Modifié le</value></data>
<data name="Close" xml:space="preserve"><value>Fermer</value></data>
<data name="CloseLanguageMenu" xml:space="preserve"><value>Fermer le menu des langues</value></data>
<data name="ConfirmSignature" xml:space="preserve"><value>Confirmer la signature</value></data>
<data name="Created" xml:space="preserve"><value>Créé</value></data>
<data name="Delete" xml:space="preserve"><value>Supprimer</value></data>
<data name="Document" xml:space="preserve"><value>Document</value></data>
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Le document n'a pas pu être chargé</value></data>
<data name="DocumentLoading" xml:space="preserve"><value>Chargement du document...</value></data>
<data name="EmailAddress" xml:space="preserve"><value>Adresse e-mail</value></data>
<data name="Envelope" xml:space="preserve"><value>Enveloppe</value></data>
<data name="Error" xml:space="preserve"><value>Erreur</value></data>
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Erreur lors du chargement du document : {0}</value></data>
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Mode développement</value></data>
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Cela peut afficher des informations sensibles d'exception aux utilisateurs finaux. Pour le débogage local, définissez ASPNETCORE_ENVIRONMENT sur Development et redémarrez l'application.</value></data>
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Le mode développement affiche des informations d'erreur plus détaillées.</value></data>
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Le mode développement ne doit pas être activé dans les applications déployées.</value></data>
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Une erreur s'est produite lors du traitement de votre demande.</value></data>
<data name="ErrorPage_RequestId" xml:space="preserve"><value>ID de requête</value></data>
<data name="ErrorWithReason" xml:space="preserve"><value>Erreur : {0}</value></data>
<data name="From" xml:space="preserve"><value>De</value></data>
<data name="FullName" xml:space="preserve"><value>Nom et prénom</value></data>
<data name="History" xml:space="preserve"><value>Historique</value></data>
<data name="Index_DigitalSignature" xml:space="preserve"><value>Signature numérique</value></data>
<data name="Index_PdfExport" xml:space="preserve"><value>Export PDF</value></data>
<data name="Index_SecureAccess" xml:space="preserve"><value>Accès sécurisé</value></data>
<data name="Index_Subtitle" xml:space="preserve"><value>Bienvenue sur le portail eSign</value></data>
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Aller à l'aperçu des enveloppes</value></data>
<data name="Languages" xml:space="preserve"><value>Langues</value></data>
<data name="Loading" xml:space="preserve"><value>Chargement...</value></data>
<data name="Login" xml:space="preserve"><value>Connexion</value></data>
<data name="LoginInProgress" xml:space="preserve"><value>Connexion ...</value></data>
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Non authentifié. Veuillez vous reconnecter.</value></data>
<data name="Only" xml:space="preserve"><value>Uniquement</value></data>
<data name="OpenDocument" xml:space="preserve"><value>Ouvrir le document</value></data>
<data name="Optional" xml:space="preserve"><value>optionnel</value></data>
<data name="Pending" xml:space="preserve"><value>En attente</value></data>
<data name="Preview" xml:space="preserve"><value>Aperçu</value></data>
<data name="PrivacyPolicy" xml:space="preserve"><value>Politique de confidentialité</value></data>
<data name="Receiver" xml:space="preserve"><value>Destinataire</value></data>
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Saisir le code d'accès</value></data>
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Veuillez saisir le code d'accès reçu par e-mail pour ouvrir le document en toute sécurité.</value></data>
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'expéditeur du document.</value></data>
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Le code saisi est incorrect. Veuillez réessayer.</value></data>
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Code d'accès invalide.</value></data>
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Le code d'accès indiqué ne correspond à aucun document. Veuillez vérifier le lien dans votre e-mail.</value></data>
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document introuvable.</value></data>
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé avec code d'accès</value></data>
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Cette action est irréversible. En confirmant, vous déclarez avoir signé électroniquement le document affiché ci-dessus. Le document signé sera ensuite transmis à toutes les parties concernées.</value></data>
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Voulez-vous signer ce document de manière contraignante ?</value></data>
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>La clé d'enveloppe est manquante.</value></data>
<data name="Refresh" xml:space="preserve"><value>Actualiser</value></data>
<data name="RequiredField" xml:space="preserve"><value>Champ obligatoire</value></data>
<data name="Selected" xml:space="preserve"><value>sélectionnées</value></data>
<data name="Send" xml:space="preserve"><value>Envoyer</value></data>
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Enveloppes actives</value></data>
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Enveloppes terminées</value></data>
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Nouvelle enveloppe</value></data>
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Créer une nouvelle enveloppe</value></data>
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Supprimer l'enveloppe</value></data>
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Saisir la raison...</value></data>
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Veuillez indiquer la raison pour laquelle l'enveloppe doit être supprimée/retirée.</value></data>
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Veuillez indiquer un motif de suppression.</value></data>
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Supprimer l'enveloppe sélectionnée</value></data>
<data name="Sender_Edit" xml:space="preserve"><value>Modifier</value></data>
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Modifier l'enveloppe sélectionnée</value></data>
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Exporter la vue actuelle</value></data>
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Sélectionner le format d'export</value></data>
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Exporter la vue actuelle du tableau</value></data>
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Erreur lors du chargement des enveloppes</value></data>
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Chargement des enveloppes...</value></data>
<data name="Sender_NoHistory" xml:space="preserve"><value>Aucun historique disponible</value></data>
<data name="Sender_NoReceivers" xml:space="preserve"><value>Aucun destinataire</value></data>
<data name="Sender_Preview_Title" xml:space="preserve"><value>Afficher l'enveloppe sélectionnée en PDF</value></data>
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Le document de l'enveloppe #{0} est introuvable ou vous n'avez pas l'autorisation d'y accéder.</value></data>
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Ajouter une signature</value></data>
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Cliquez sur l'emplacement dans le document pour</value></data>
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Les adresses e-mail déjà utilisées sont proposées pendant la saisie.</value></data>
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Chargement des suggestions d'e-mail...</value></data>
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Erreur lors de la création de l'enveloppe</value></data>
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Erreur lors du chargement de l'enveloppe #{0} : {1}</value></data>
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Erreur lors du chargement du fichier : {0}</value></data>
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Veuillez signer le document ci-joint.</value></data>
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Aucun document chargé</value></data>
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Aucune page à afficher.</value></data>
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Aucun destinataire ajouté pour le moment.</value></data>
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Placer un champ de signature</value></data>
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Supprimer tous les champs de signature</value></data>
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Veuillez sélectionner un fichier PDF.</value></data>
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Le brouillon existant a été envoyé.</value></data>
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Enveloppe envoyée</value></data>
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>L'enveloppe a été envoyée avec succès.</value></data>
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Champ de signature</value></data>
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>p. ex. Contrat de travail 2025</value></data>
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Vers l'aperçu</value></data>
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Téléversez un fichier PDF pour placer des champs de signature.</value></data>
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Cette adresse e-mail a déjà été ajoutée.</value></data>
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Veuillez saisir une adresse e-mail valide.</value></data>
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Veuillez saisir une adresse e-mail.</value></data>
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Veuillez saisir le nom et le prénom.</value></data>
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Veuillez ajouter au moins un destinataire.</value></data>
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Veuillez placer au moins un champ de signature.</value></data>
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Veuillez saisir un titre.</value></data>
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Veuillez d'abord téléverser un document PDF.</value></data>
<data name="SenderLogin_Description" xml:space="preserve"><value>Veuillez vous connecter avec vos identifiants pour accéder au tableau de bord expéditeur.</value></data>
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'administrateur.</value></data>
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Le nom d'utilisateur ou le mot de passe est incorrect. Veuillez réessayer.</value></data>
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Identifiants invalides.</value></data>
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Saisir le mot de passe</value></data>
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé au tableau de bord expéditeur</value></data>
<data name="SenderLogin_Title" xml:space="preserve"><value>Connexion expéditeur</value></data>
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Saisir le nom d'utilisateur</value></data>
<data name="ServerError" xml:space="preserve"><value>Erreur serveur.</value></data>
<data name="Signed" xml:space="preserve"><value>Signé</value></data>
<data name="SignedDocument" xml:space="preserve"><value>Document signé</value></data>
<data name="System" xml:space="preserve"><value>Système</value></data>
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Une erreur inattendue s'est produite. Veuillez réessayer plus tard.</value></data>
<data name="UploadPdf" xml:space="preserve"><value>Téléverser un PDF</value></data>
<data name="UserName" xml:space="preserve"><value>Nom d'utilisateur</value></data>
<data name="VerifyInProgress" xml:space="preserve"><value>Vérification ...</value></data>
</root> </root>

View File

@@ -2,8 +2,7 @@
{ {
public enum CertificationType public enum CertificationType
{ {
AdvancedElectronicSignature = 1 AdvancedElectronicSignature = 1,
// ElectronicSignature = 1 ElectronicSignature = 2
// QualifiedSignature = 2
} }
} }

View File

@@ -67,7 +67,7 @@ namespace EnvelopeGenerator.Domain.Constants
{ {
public static bool IsActive(this EnvelopeStatus status) public static bool IsActive(this EnvelopeStatus status)
{ {
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned; return status >= EnvelopeStatus.EnvelopeCreated && status <= EnvelopeStatus.EnvelopePartlySigned;
} }
public static bool IsCompleted(this EnvelopeStatus status) public static bool IsCompleted(this EnvelopeStatus status)

View File

@@ -11,7 +11,7 @@ using System.Collections.Generic;
namespace EnvelopeGenerator.Domain.Entities namespace EnvelopeGenerator.Domain.Entities
{ {
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")] [Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
public class DocReceiverElement : IDocReceiverElement, IHasReceiver, IHasAddedWhen, IUpdateAuditable public class DocReceiverElement : IDocReceiverElement, ISignatureInkPayload, IHasReceiver, IHasAddedWhen, IUpdateAuditable
{ {
public DocReceiverElement() public DocReceiverElement()
{ {

View File

@@ -30,7 +30,7 @@ namespace EnvelopeGenerator.Domain.Entities
SendReminderEmails = false; SendReminderEmails = false;
FirstReminderDays = 0; FirstReminderDays = 0;
ReminderIntervalDays = 0; ReminderIntervalDays = 0;
CertificationType = (int)Constants.CertificationType.AdvancedElectronicSignature; CertificationType = Constants.CertificationType.AdvancedElectronicSignature;
UseAccessCode = false; UseAccessCode = false;
Documents = Enumerable.Empty<Document>().ToList(); Documents = Enumerable.Empty<Document>().ToList();
Histories = Enumerable.Empty<History>().ToList(); Histories = Enumerable.Empty<History>().ToList();
@@ -83,7 +83,7 @@ namespace EnvelopeGenerator.Domain.Entities
public string Comment { get; set; } public string Comment { get; set; }
[Column("CONTRACT_TYPE")] [Column("CONTRACT_TYPE")]
public int? ContractType { get; set; } public ContractType? ContractType { get; set; }
#if NETFRAMEWORK #if NETFRAMEWORK
[NotMapped] [NotMapped]
@@ -111,7 +111,7 @@ namespace EnvelopeGenerator.Domain.Entities
public bool ReadOnly => this.IsReadAndConfirm(); public bool ReadOnly => this.IsReadAndConfirm();
[Column("CERTIFICATION_TYPE")] [Column("CERTIFICATION_TYPE")]
public int? CertificationType { get; set; } public CertificationType? CertificationType { get; set; }
[Column("USE_ACCESS_CODE")] [Column("USE_ACCESS_CODE")]
public bool? UseAccessCode { get; set; } public bool? UseAccessCode { get; set; }
@@ -129,7 +129,11 @@ namespace EnvelopeGenerator.Domain.Entities
public int? ExpiresWarningWhenDays { get; set; } public int? ExpiresWarningWhenDays { get; set; }
[ForeignKey("UserId")] [ForeignKey("UserId")]
public User User { get; set; } public User
#if nullable
?
#endif
User { get; set; }
[Column("TFA_ENABLED")] [Column("TFA_ENABLED")]
public bool? TfaEnabled { get; set; } public bool? TfaEnabled { get; set; }
@@ -137,7 +141,6 @@ namespace EnvelopeGenerator.Domain.Entities
= false; = false;
#endif #endif
[NotMapped]
[Column("DOC_RESULT")] [Column("DOC_RESULT")]
public byte[] public byte[]
#if nullable #if nullable

View File

@@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using EnvelopeGenerator.Domain.Constants;
#if NETFRAMEWORK #if NETFRAMEWORK
using System; using System;
#endif #endif
@@ -22,41 +24,41 @@ namespace EnvelopeGenerator.Domain.Entities
public string Language { get; set; } public string Language { get; set; }
[Column("EXPIRES_DAYS")] [Column("EXPIRES_DAYS")]
public int ExpiresDays { get; set; } public int? ExpiresDays { get; set; }
[Column("CERTIFICATION_TYPE")] [Column("CERTIFICATION_TYPE")]
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature; public Constants.CertificationType? CertificationType { get; set; }
[Column("USE_ACCESS_CODE")] [Column("USE_ACCESS_CODE")]
public bool UseAccessCode { get; set; } public bool? UseAccessCode { get; set; }
[Column("FINAL_EMAIL_TO_CREATOR")] [Column("FINAL_EMAIL_TO_CREATOR")]
public Constants.FinalEmailType FinalEmailToCreator { get; set; } public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
[Column("FINAL_EMAIL_TO_RECEIVERS")] [Column("FINAL_EMAIL_TO_RECEIVERS")]
public Constants.FinalEmailType FinalEmailToReceivers { get; set; } public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
[Required] [Required]
[Column("ADDED_WHEN", TypeName = "datetime")] [Column("ADDED_WHEN", TypeName = "datetime")]
public DateTime AddedWhen { get; set; } public DateTime AddedWhen { get; set; }
[Column("CHANGED_WHEN", TypeName = "datetime")] [Column("CHANGED_WHEN", TypeName = "datetime")]
public DateTime ChangedWhen { get; set; } public DateTime? ChangedWhen { get; set; }
[Column("EXPIRES_WARNING_DAYS")] [Column("EXPIRES_WARNING_DAYS")]
public int ExpiresWarningDays { get; set; } public int? ExpiresWarningDays { get; set; }
[Column("SEND_REMINDER_EMAILS")] [Column("SEND_REMINDER_EMAILS")]
public bool SendReminderEmails { get; set; } = false; public bool? SendReminderEmails { get; set; }
[Column("FIRST_REMINDER_DAYS")] [Column("FIRST_REMINDER_DAYS")]
public int FirstReminderDays { get; set; } = 0; public int? FirstReminderDays { get; set; }
[Column("REMINDER_INTERVAL_DAYS")] [Column("REMINDER_INTERVAL_DAYS")]
public int ReminderIntervalDays { get; set; } = 0; public int? ReminderIntervalDays { get; set; }
[Column("CONTRACT_TYPE")] [Column("CONTRACT_TYPE")]
public int ContractType { get; set; } public ContractType? ContractType { get; set; }
[NotMapped] [NotMapped]
public bool TFA_Enabled { get; set; } = false; public bool TFA_Enabled { get; set; } = false;
@@ -70,14 +72,14 @@ namespace EnvelopeGenerator.Domain.Entities
[NotMapped] [NotMapped]
public int ExpiresWhenDays public int ExpiresWhenDays
{ {
get => ExpiresDays; get => ExpiresDays ?? 0;
set => ExpiresDays = value; set => ExpiresDays = value;
} }
[NotMapped] [NotMapped]
public int ExpiresWarningWhenDays public int ExpiresWarningWhenDays
{ {
get => ExpiresWarningDays; get => ExpiresWarningDays ?? 0;
set => ExpiresWarningDays = value; set => ExpiresWarningDays = value;
} }

View File

@@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net462;net7.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net462;net7.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<AssemblyVersion>1.4.0</AssemblyVersion> <AssemblyVersion>1.4.0</AssemblyVersion>
<FileVersion>1.4.0</FileVersion> <FileVersion>1.4.0</FileVersion>
</PropertyGroup> </PropertyGroup>

View File

@@ -0,0 +1,17 @@
#if NETFRAMEWORK
using System;
#endif
namespace EnvelopeGenerator.Domain.Exceptions
{
public class AuthorizationException : Exception
{
public AuthorizationException(string message) : base(message)
{
}
public AuthorizationException()
{
}
}
}

View File

@@ -0,0 +1,17 @@
#if NETFRAMEWORK
using System;
#endif
namespace EnvelopeGenerator.Domain.Exceptions
{
public class DataIntegrityException : Exception
{
public DataIntegrityException(string message) : base(message)
{
}
public DataIntegrityException() : base()
{
}
}
}

View File

@@ -3,6 +3,12 @@
public interface IEnvelope public interface IEnvelope
{ {
int? EnvelopeTypeId { get; set; } int? EnvelopeTypeId { get; set; }
byte[]
#if nullable
?
#endif
DocResult { get; }
} }
public static class EnvelopeExtensions public static class EnvelopeExtensions
@@ -16,5 +22,10 @@
{ {
return envelope.EnvelopeTypeId != 2; return envelope.EnvelopeTypeId != 2;
} }
public static bool IsBurned(this IEnvelope envelope)
{
return envelope.DocResult != null;
}
} }
} }

View File

@@ -0,0 +1,28 @@
#if NETFRAMEWORK
using System;
#endif
namespace EnvelopeGenerator.Domain.Interfaces
{
public interface ISignatureInkPayload
{
#if nullable
byte[]? Ink { get; }
string? FullName { get; }
string? Position { get; }
string? Place { get; }
#else
byte[] Ink { get; }
string FullName { get; }
string Position { get; }
string Place { get; }
#endif
DateTime? ChangedWhen { get; }
}
}

View File

@@ -0,0 +1,7 @@
namespace EnvelopeGenerator.Domain.Resources
{
// Marker type for IStringLocalizer lookup of Resources/Model.resx.
public class Model
{
}
}

View File

@@ -276,4 +276,61 @@
<data name="YesWithAttachment" xml:space="preserve"> <data name="YesWithAttachment" xml:space="preserve">
<value>Yes, with Attachment</value> <value>Yes, with Attachment</value>
</data> </data>
<data name="ElectronicSignature" xml:space="preserve">
<value>Electronic Signature</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Envelope Type</value>
</data>
<data name="Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Certification</value>
</data>
<data name="TFAEnabled" xml:space="preserve">
<value>2FA enabled</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Use Access Code</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>Final Email to Creator</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>Final Email to Receivers</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Send Reminder Emails</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>First Reminder (Days)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Reminder Interval (Days)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Expires After (Days)</value>
</data>
<data name="ExpiresWarningWhenDays" xml:space="preserve">
<value>Expiry Warning (Days)</value>
</data>
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
<value>Envelope Settings</value>
</data>
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
<value>Advanced</value>
</data>
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
<value>Select Template</value>
</data>
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
<value>Please enter a reminder interval greater than 0 days.</value>
</data>
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
<value>Please enter valid days for the first reminder.</value>
</data>
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
<value>The expiry warning cannot be after the expiry date.</value>
</data>
</root> </root>

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AccessCodeIncorrect" xml:space="preserve">
<value>Code d'acces incorrect</value>
</data>
<data name="DocumentRejected" xml:space="preserve">
<value>Document rejete</value>
</data>
<data name="DocumentSigned" xml:space="preserve">
<value>Document signe</value>
</data>
<data name="Draft" xml:space="preserve">
<value>Brouillon</value>
</data>
<data name="EnvelopeCompletelySigned" xml:space="preserve">
<value>Entierement signe</value>
</data>
<data name="EnvelopeCreated" xml:space="preserve">
<value>Enveloppe creee</value>
</data>
<data name="EnvelopeDeleted" xml:space="preserve">
<value>Enveloppe supprimee</value>
</data>
<data name="EnvelopePartlySigned" xml:space="preserve">
<value>Partiellement signee</value>
</data>
<data name="EnvelopeQueued" xml:space="preserve">
<value>Enveloppe en file d'attente</value>
</data>
<data name="EnvelopeRejected" xml:space="preserve">
<value>Enveloppe rejetee</value>
</data>
<data name="EnvelopeSaved" xml:space="preserve">
<value>Enregistree</value>
</data>
<data name="EnvelopeSent" xml:space="preserve">
<value>Envoyee</value>
</data>
<data name="EnvelopeWithdrawn" xml:space="preserve">
<value>Enveloppe retiree</value>
</data>
<data name="MessageAccessCodeSent" xml:space="preserve">
<value>Code d'acces envoye</value>
</data>
<data name="MessageCompletionSent" xml:space="preserve">
<value>E-mail de cloture envoye</value>
</data>
<data name="MessageConfirmationSent" xml:space="preserve">
<value>Confirmation finale envoyee</value>
</data>
<data name="MessageDeletionSent" xml:space="preserve">
<value>Notification de suppression envoyee</value>
</data>
<data name="MessageInvitationSent" xml:space="preserve">
<value>Invitation envoyee</value>
</data>
<data name="ElectronicSignature" xml:space="preserve">
<value>Signature electronique</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Type d'enveloppe</value>
</data>
<data name="Language" xml:space="preserve">
<value>Langue</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Certification</value>
</data>
<data name="TFAEnabled" xml:space="preserve">
<value>2FA active</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Utiliser un code d'acces</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>E-mail final au createur</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>E-mail final aux destinataires</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Envoyer des e-mails de rappel</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>Premier rappel (jours)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Intervalle de rappel (jours)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Expire apres (jours)</value>
</data>
<data name="ExpiresWarningWhenDays" xml:space="preserve">
<value>Avertissement d'expiration (jours)</value>
</data>
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
<value>Parametres de l'enveloppe</value>
</data>
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
<value>Avance</value>
</data>
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
<value>Selectionner un modele</value>
</data>
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
<value>Veuillez saisir un intervalle de rappel superieur a 0 jour.</value>
</data>
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
<value>Veuillez saisir des jours valides pour le premier rappel.</value>
</data>
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
<value>L'avertissement d'expiration ne peut pas etre apres la date d'expiration.</value>
</data>
</root>

View File

@@ -279,4 +279,61 @@
<data name="YesWithAttachment" xml:space="preserve"> <data name="YesWithAttachment" xml:space="preserve">
<value>Ja, mit Anhang</value> <value>Ja, mit Anhang</value>
</data> </data>
<data name="ElectronicSignature" xml:space="preserve">
<value>Elektronische Signatur</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Umschlagtyp</value>
</data>
<data name="Language" xml:space="preserve">
<value>Sprache</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Zertifizierung</value>
</data>
<data name="TFAEnabled" xml:space="preserve">
<value>2FA aktiviert</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Zugriffscode verwenden</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>Finale E-Mail an Ersteller</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>Finale E-Mail an Empfänger</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Erinnerungs-E-Mails senden</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>Erste Erinnerung (Tage)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Erinnerungsintervall (Tage)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Läuft ab nach (Tage)</value>
</data>
<data name="ExpiresWarningWhenDays" xml:space="preserve">
<value>Ablaufwarnung (Tage)</value>
</data>
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
<value>Envelope-Einstellungen</value>
</data>
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
<value>Erweitert</value>
</data>
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
<value>Vorlage auswählen</value>
</data>
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
<value>Bitte geben Sie ein Erinnerungsintervall größer als 0 Tage ein.</value>
</data>
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
<value>Bitte geben Sie gültige Tage für die erste Erinnerung ein.</value>
</data>
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
<value>Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.</value>
</data>
</root> </root>

View File

@@ -105,10 +105,10 @@ Public Class frmEnvelopeMainData
End If End If
End Sub End Sub
Private Function TranslateCertificationType(pType As CertificationType) As String Private Function TranslateCertificationType(pType As CertificationType) As String
Return Resources.Model.ResourceManager.GetString(pType.ToString()) Return CommonServices.My.Resources.Model.ResourceManager.GetString(pType.ToString())
End Function End Function
Private Function TranslateFinalEmailType(pType As FinalEmailType) As String Private Function TranslateFinalEmailType(pType As FinalEmailType) As String
Return Resources.Model.ResourceManager.GetString(pType.ToString()) Return CommonServices.My.Resources.Model.ResourceManager.GetString(pType.ToString())
End Function End Function
Private Sub btOK_Click(sender As Object, e As EventArgs) Handles btOK.Click Private Sub btOK_Click(sender As Object, e As EventArgs) Handles btOK.Click

View File

@@ -0,0 +1,23 @@
using DocumentService.Client.Configuration;
using DocumentService.Client.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Infrastructure.Crypt.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Infrastructure.Crypt.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCryptInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
var baseUrl = configuration["DocumentService:BaseUrl"]
?? throw new InvalidOperationException("DocumentService:BaseUrl is not configured.");
services.AddDocumentServiceClients(options => options.BaseUrl = baseUrl);
services.AddSingleton<IPdfDigitalSignatureService, PdfDigitalSignatureService>();
return services;
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DocumentService.Client" Version="1.3.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,384 @@
# EnvelopeGenerator.Infrastructure.Crypt
Umfassende technische Dokumentation der PKCS#11/SoftHSM-Integration fuer `EnvelopeGenerator`.
Dieses Dokument konsolidiert und ersetzt die verteilten Runbooks aus:
- `pkcs11-proxy-production/README.md`
- `pkcs11-proxy-production/SSH_TEMP_ACCESS_MUST_DO.md`
- aeltere Abschnitte in diesem README
Zielgruppe: Senior Developer, Platform Engineers, DevOps, Security Owner.
---
## 1. Architekturueberblick
### 1.1 Ziel
`EnvelopeGenerator.Infrastructure.Crypt` stellt die kryptographische Infrastruktur fuer PDF-Signaturen bereit:
- Signaturerzeugung mit Schluesseln in SoftHSM/PKCS#11
- Zertifikat-Auslese aus Token
- Signaturpruefung auf PDF-Ebene
- Health/Startup-Selbsttest fuer Betriebsfaehigkeit
### 1.2 Topologie (aktueller Betriebsmodus)
Der aktuell implementierte Produktions-nahe Pfad ist:
1. Linux hostet SoftHSM + `pkcs11-daemon` (Proxy-Server).
2. Windows hostet `EnvelopeGenerator.Server`.
3. Windows laedt ein Windows-kompatibles Proxy-PKCS#11-Modul (`libpkcs11-proxy.dll`).
4. PKCS#11-Operationen werden ueber Socket (`PKCS11_PROXY_SOCKET`) an Linux weitergeleitet.
Wichtig:
- Ein Windows-Prozess kann **kein Linux `.so`** direkt laden.
- Private Keys verbleiben im Token (nicht exportierbar).
### 1.3 Laufzeitpfade in der Anwendung
`Pkcs11InteropBootstrap` loest Assets relativ zu `Crypt:Pkcs11:AssetsRootPath` auf:
- Modul: `pkcs11\windows\proxy\libpkcs11-proxy.dll`
- Tool-Fallback: `pkcs11\windows\opensc\tools\pkcs11-tool.exe`
Wenn `AssetsRootPath` leer ist, wird `AppContext.BaseDirectory` verwendet.
---
## 2. Linux-Seite: SoftHSM + Proxy Setup
### 2.1 Basispakete
Beispiel (Debian/Ubuntu):
```bash
sudo apt-get update && sudo apt-get install -y softhsm2 opensc p11-kit libengine-pkcs11-openssl
```
### 2.2 SoftHSM-Modulpfad pruefen
```bash
ldconfig -p | grep softhsm
find /usr -name "libsofthsm2.so" 2>/dev/null
```
### 2.3 Token initialisieren
```bash
softhsm2-util --show-slots
softhsm2-util --init-token --slot <uninitialized-slot> --label CompanyProdToken --so-pin <so-pin> --pin <user-pin>
softhsm2-util --show-slots
```
Hinweis: Nach `--init-token` wird der Slot i. d. R. neu zugewiesen.
### 2.4 Schluesselpaar erzeugen
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
--slot <slot-id> --login --pin <user-pin> \
--keypairgen --key-type rsa:3072 --label company-sign-key-v1 --id 01
```
### 2.5 Zertifikat importieren
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
--slot <slot-id> --login --pin <user-pin> \
--write-object ./company-sign-cert-v1.der --type cert --id 01 --label company-sign-cert-v1
```
### 2.6 Objektzustand verifizieren
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> --list-objects
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> --list-objects --type cert
```
### 2.7 Proxy-Daemon endpoint stabilisieren
Ein wiederkehrendes Betriebsproblem war ein zufaelliger/local-only Listener. Fix:
```bash
sudo mkdir -p /etc/systemd/system/pkcs11-proxy.service.d
printf "[Service]\nEnvironment=PKCS11_DAEMON_SOCKET=tcp://0.0.0.0:5657\n" | sudo tee /etc/systemd/system/pkcs11-proxy.service.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart pkcs11-proxy.service
```
Pruefung:
```bash
systemctl show pkcs11-proxy.service -p Environment --no-pager
ss -ltnp | grep 5657
```
---
## 3. Web-API/Server-Seite: Installation und Asset-Bereitstellung
### 3.1 Bezugsquellen der Windows-Artefakte
Fuer den Proxy-Betrieb werden auf Windows benoetigt:
1. Proxy-PKCS#11-DLL (`libpkcs11-proxy.dll`)
2. Abhaengige Runtime-DLLs (z. B. MinGW runtime, je nach Build)
3. `pkcs11-tool.exe` + zugehoerige OpenSC-DLLs fuer Fallback
Beschaffung:
- Build aus derselben `pkcs11-proxy`-Codebasis/Commit wie Linux-Daemon (Kompatibilitaet sicherstellen)
- oder kontrolliert bereitgestellte, intern freigegebene Binaries
### 3.2 Empfohlene Ablage im Repo
Bundled Assets liegen unter:
- `EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/`
- `EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/`
- `EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/pkcs11/`
Diese werden beim Build in den Output kopiert (siehe `.csproj`-Copy-Regeln).
### 3.3 Deployment-Muster
Es gibt zwei gueltige Wege:
1. **Bundled (Dev/QA, schnelle Reproduzierbarkeit)**
- Assets werden mit dem Build ausgeliefert.
- `AssetsRootPath = null` -> Aufloesung via `AppContext.BaseDirectory`.
2. **Zentraler Asset-Root (Prod, betriebliche Kontrolle)**
- Assets in zentralem Pfad (z. B. `E:\DataFiles\Digital Data\EnvelopeGenerator`).
- `AssetsRootPath` in `appsettings.json` auf diesen Root setzen.
### 3.4 Anwendungskonfiguration (`Crypt:Pkcs11`)
Beispiel:
```json
{
"Crypt": {
"Pkcs11": {
"AssetsRootPath": "E:\\DataFiles\\Digital Data\\EnvelopeGenerator",
"SlotId": 2021549410,
"TokenLabel": "CompanyProdToken",
"ProxySocket": "tcp://172.24.12.64:5657",
"UserPin": "<secret>",
"PrivateKeyLabel": "company-sign-key-v1",
"CertificateLabel": "company-sign-cert-v1",
"ApplicationName": "signFLOW",
"DefaultReason": null,
"UseUtcSigningTime": true,
"DefaultCertificationLevel": "NoCertification"
}
}
}
```
Sicherheitsregel:
- `UserPin` niemals in versionierte Dateien einchecken.
- Secrets nur in Secret Store / nicht versionierte `appsettings.Secrets.json` / Vault.
---
## 4. Implementierte Services, Modelle, DTOs
### 4.1 DI und Startverhalten
- `AddCryptInfrastructure(...)` in `DependencyInjection/ServiceCollectionExtensions.cs`
- bindet `Pkcs11Options`
- registriert `IPdfDigitalSignatureService`
- registriert Startup-Self-Test Hosted Service
### 4.2 Konfigurationsmodell
- `Pkcs11Options` (`EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs`)
- `AssetsRootPath`, `SlotId`, `TokenLabel`, `ProxySocket`
- `UserPin`, `PrivateKeyLabel`, `CertificateLabel`
- `ApplicationName`, `DefaultReason`, `UseUtcSigningTime`, `DefaultCertificationLevel`
- `ResolvedLibraryPath`, `ResolvedToolPath` (Runtime-resolved)
### 4.3 Kernservices
1. `PdfDigitalSignatureService`
- Signiert PDF via DevExpress `PdfDeferredSignatureBuilder`
- Nutzt `Pkcs11ExternalSigner` fuer externe Signatur
- Erzwingt `ApplicationName` aus Config (nicht client-overridable)
- Erweitert `Reason` um strukturiertes Metaformat:
- `SFMETA:v1;doc=<uuid>;sha256=<hash>`
- Parse beim Verify und Mapping in DTO
2. `Pkcs11InteropBootstrap`
- Loest Modul-/Toolpfade auf
- Setzt `PKCS11_PROXY_SOCKET`/`PKCS11_DAEMON_SOCKET`
- PATH-Ergaenzung fuer native Abhaengigkeiten
- Warmup via `pkcs11-tool -L`
3. `Pkcs11SessionRunner`
- Interop Library-Load mit mehreren Init-Modi (Fallback-Reihenfolge)
- Slot-Resolution (`SlotId`/`TokenLabel`)
- Session Login/Logout
4. `Pkcs11ToolBridge`
- Fallback via Prozessaufruf von `pkcs11-tool`
- Cert read und DigestInfo-Signatur
5. `Pkcs11StartupSelfTestHostedService`
- Fuehrt beim Start Interop-Probe aus
- Fallback-Probe bei Interop-Fehler
- Schreibt strukturierten Status
### 4.4 Wichtige Interfaces/DTOs
- `IPdfDigitalSignatureService`
- `SignPdfAsync(...)`, `CheckPdfSignatureAsync(...)`, `ExtractOriginalPdfAsync(...)`
- `IPkcs11SelfTestStatusProvider`
- `PdfSignOptionsDto`
- `UUID`, `Reason`, `Name`, `Location`, `Email`, `Phone`, `CertificationLevel`
- `PdfSignatureCheckResultDto` / `PdfSignatureCheckItemDto`
- inklusive `BusinessReason`, `UUID`, `OriginalDocumentSha256`
- `Pkcs11SelfTestStatusDto`
---
## 5. Betriebserfahrungen und Challenges
### 5.1 Interop vs Tool-Verhalten
Beobachtetes Muster in der realen Umgebung:
- `pkcs11-tool` konnte erfolgreich auf Token zugreifen und signieren.
- `Pkcs11Interop` scheiterte mit `CKR_FUNCTION_NOT_SUPPORTED` bzw. fehlendem `C_Initialize` Pointer in bestimmten Modi.
Konsequenz:
- Architektur wurde auf **interop-first + controlled tool-fallback** erweitert.
- Health-Endpunkt differenziert Modus (`interop`, `pkcs11-tool`, `failed`).
### 5.2 Typische Fehlbilder
- `CKR_DEVICE_ERROR` bei Socket-/Proxy-Problemen
- `CRYPT_PROVIDER_UNAVAILABLE` bei Pfad/ABI/Dependency-Mismatch
- `CRYPT_SLOT_NOT_FOUND` bei Slot-/Label-Mismatch
- `CKR_PIN_INCORRECT` bei PIN-Fehler
- 307 Redirects bei HTTP/HTTPS-Mix in manuellen Tests
### 5.3 Plattformfallen
- MSVC vs MinGW Build-Unterschiede fuer Proxy-Client
- fehlende Laufzeit-DLLs (`PATH`/Side-by-side)
- Architektur-Mismatch x86 vs x64
- Daemon-Bindings nicht stabil (random localhost port statt fixed endpoint)
---
## 6. Health, Monitoring und Verifikation
### 6.1 API Health
- Endpoint: `GET /api/health/crypt`
- Liefert:
- `success`, `mode`, `summary`, `interopError`, `toolError`, `checkedAtUtc`
Empfehlung:
- In Monitoring aufnehmen und bei `mode=pkcs11-tool` ueber laengere Zeit Warnung triggern.
### 6.2 Smoke-Test (empfohlen vor Release)
1. `GET /api/health/crypt` -> `success=true`
2. `POST /api/PdfSignature/sign` mit Test-PDF
3. `POST /api/PdfSignature/is-signed`
4. Pruefen:
- `signed=true`
- `allSignaturesCryptographicallyValid=true`
- `UUID`/`OriginalDocumentSha256` korrekt gemappt
---
## 7. Sicherheits- und Betriebsrichtlinien
### 7.1 Geheimnisse
- PIN, SO-PIN, Root-Creds niemals in Doku/Screenshots/Repo.
- Nach Troubleshooting: Credentials rotieren.
### 7.2 Temporare SSH-Freigaben zwingend rueckbauen
Wenn SSH temporaer geoeffnet wurde, MUSS danach Rollback erfolgen:
```bash
sudo systemctl disable --now ssh && sudo ufw delete allow from 172.24.10.11 to any port 22 proto tcp && sudo ufw status verbose && sudo systemctl status ssh --no-pager
```
Optional harte Bereinigung:
```bash
sudo apt-get purge -y openssh-server && sudo apt-get autoremove -y
```
### 7.3 Schluesselrotation
- Neue Labels (`...-v2`) anlegen
- App umkonfigurieren
- Sign/Verify Regression testen
- Altmaterial erst nach Freigabe entfernen
---
## 8. API-Verhalten bei Signaturmetadaten
Aktueller Standard:
- `UUID` kommt aus Sign-Input (nullable string). Wenn ungeltig/leer -> neue Guid wird erzeugt.
- `Reason` wird intern angereichert mit strukturiertem Meta (`SFMETA:v1`).
- `ApplicationName` ist rein serverseitig aus `appsettings` gesetzt.
- `SigningTime` ist rein serverseitig und wird automatisch gesetzt (`UseUtcSigningTime=true` => `UtcNow`, sonst lokale Zeit).
- `CertificationLevel` wird im Controller serverseitig gesetzt (`FormFillingAndAnnotations`), nicht vom Client.
Damit ist das Verhalten deterministisch und manipulationsaermer fuer Integrationsclients.
---
## 9. Troubleshooting-Checkliste (praxisnah)
1. Erreichbarkeit Linux Proxy:
- `Test-NetConnection <linux-host> -Port 5657`
2. Token sichtbar?
- `pkcs11-tool --module <dll> -L -v`
3. Zertifikat lesbar?
- `--read-object --type cert --label <cert-label>`
4. Signatur technisch moeglich?
- `--sign -m SHA256-RSA-PKCS ...`
5. App-Health check:
- `/api/health/crypt`
6. App-sign/verify end-to-end:
- `/api/PdfSignature/sign` + `/api/PdfSignature/is-signed`
Wenn Interop scheitert, aber Tool funktioniert:
- Betrieb bleibt verfuegbar (Fallback),
- aber Incident/Bug fuer Interop-Kompatibilitaet erfassen.
---
## 10. Ausblick / empfohlene Verbesserungen
1. Binary Transport statt Base64 bei grossen PDFs in Render/Sign-Pipelines.
2. Persistente Metriken fuer Fallback-Quote (`interop` vs `tool`).
3. Zertifikatskette/OCSP/CRL Validierung als optionales Policy-Modul.
4. CI-Pruefung auf unsichere SQL-Muster (parallel zum Security-Workstream).
5. Standardisierte Toast/UX-Rueckmeldungen fuer Signierfehler in UI.
---
## 11. Referenzen
- SoftHSMv2: `https://github.com/softhsm/SoftHSMv2`
- OpenSC / pkcs11-tool: `https://github.com/OpenSC/OpenSC/wiki/Using-pkcs11-tool-and-OpenSSL`
- PKCS#11 Standard: `https://www.oasis-open.org/committees/pkcs11/`

View File

@@ -0,0 +1,77 @@
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Requests;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using ClientCertLevel = DocumentService.Client.Models.Requests.PdfSignatureCertificationLevel;
using ClientCoordUnit = DocumentService.Client.Models.Requests.PdfSignatureCoordinateUnit;
using ClientCoordOrigin = DocumentService.Client.Models.Requests.PdfSignatureCoordinateOrigin;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
/// <summary>
/// Proxy that forwards PDF signing to the remote DocumentService API via <see cref="IPdfSignatureClient"/>.
/// </summary>
public sealed class PdfDigitalSignatureService(IPdfSignatureClient signatureClient) : IPdfDigitalSignatureService
{
public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default)
{
var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken);
var imageBytes = image is null ? null : await ReadAllBytesAsync(image, cancellationToken);
using var signed = await signatureClient.SignAsync(pdfBytes, Map(options), imageBytes, cancellationToken);
await signed.CopyToAsync(outputPdf, cancellationToken);
}
public Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default)
=> throw new NotSupportedException("Signature checking is handled by DocumentService and is not available through this proxy.");
private static SignPdfOptions Map(PdfSignOptionsDto? o) => new()
{
DocumentElementId = o?.DocumentElementId ?? 0,
Name = o?.Name ?? string.Empty,
Reason = o?.Reason,
Location = o?.Location,
Email = o?.Email,
Phone = o?.Phone,
ApplicationName = o?.ApplicationName,
CertificationLevel = (ClientCertLevel)(o?.CertificationLevel ?? EnvelopeGenerator.Application.Common.Enums.PdfSignatureCertificationLevel.NoCertification),
Appearance = o?.Appearance is null ? null : MapAppearance(o.Appearance)
};
private static PdfSignatureAppearance MapAppearance(PdfSignatureAppearanceDto a) => new()
{
PageNumber = a.PageNumber,
Left = a.Left,
Bottom = a.Bottom,
Width = a.Width,
Height = a.Height,
ShowLabels = a.ShowLabels,
ShowName = a.ShowName,
ShowDate = a.ShowDate,
ShowReason = a.ShowReason,
ShowLocation = a.ShowLocation,
ShowDistinguishedName = a.ShowDistinguishedName,
DateTimeFormat = a.DateTimeFormat,
CoordinateUnit = (ClientCoordUnit)a.CoordinateUnit,
CoordinateOrigin = (ClientCoordOrigin)a.CoordinateOrigin,
Font = new PdfSignatureFont
{
FamilyName = a.Font.FamilyName,
Size = a.Font.Size,
IsBold = a.Font.IsBold,
IsItalic = a.Font.IsItalic,
IsUnderline = a.Font.IsUnderline,
Color = new PdfSignatureColor { R = a.Font.Color.R, G = a.Font.Color.G, B = a.Font.Color.B }
}
};
private static async Task<byte[]> ReadAllBytesAsync(Stream stream, CancellationToken cancellationToken)
{
if (stream is MemoryStream memory && memory.TryGetBuffer(out var buffer))
return buffer.AsSpan(0, (int)memory.Length).ToArray();
using var copy = new MemoryStream();
await stream.CopyToAsync(copy, cancellationToken);
return copy.ToArray();
}
}

View File

@@ -0,0 +1,18 @@
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EnvelopeGenerator.Infrastructure.Doc;
public static class DependencyInjection
{
public static IServiceCollection AddDocInfrastructure(this IServiceCollection services)
{
services.AddScoped<IPdfPageImageRenderer, PdfPageImageRenderer>();
return services;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Document.Processor" Version="25.2.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,180 @@
using DevExpress.Drawing;
using DevExpress.Pdf;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Infrastructure.Doc;
public class PdfPageImageRenderer : IPdfPageImageRenderer
{
private const float RenderDpi = 110f;
public Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfBytes);
if (pdfBytes.Length == 0)
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
using var stream = new MemoryStream(pdfBytes, writable: false);
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(stream);
var pages = processor.Document.Pages;
if (pages.Count == 0)
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(RenderDpi);
var results = new List<PdfPageImageDto>(pages.Count);
for (var i = 0; i < pages.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var pageNumber = i + 1;
var page = pages[i];
var cropBox = page.CropBox;
using DXBitmap bitmap = processor.CreateDXBitmap(pageNumber, renderOptions);
using var imageStream = new MemoryStream();
bitmap.Save(imageStream, DXImageFormat.Png);
var dataUrl = $"data:image/png;base64,{Convert.ToBase64String(imageStream.ToArray())}";
results.Add(new PdfPageImageDto
{
PageNumber = pageNumber,
WidthPt = cropBox.Width,
HeightPt = cropBox.Height,
WidthPx = bitmap.Width,
HeightPx = bitmap.Height,
ImageDataUrl = dataUrl
});
}
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(results);
}
public Stream ComposeSignaturePreviewPng(ISignatureInkPayload element)
{
const double sigW = 1.77 * 72;
const double sigH = 1.96 * 72;
const double imgRatio = 0.52;
const double lineH = 11.5;
const double bgPad = 3.0;
const double padding = 3.0;
var date = element.ChangedWhen!.Value.ToString("dd.MM.yyyy");
var position = string.IsNullOrWhiteSpace(element.Position) ? null : element.Position.Trim();
var x = bgPad;
var y = bgPad;
var imgH = sigH * imgRatio;
var lineY = y + imgH + 1.0;
var textY = lineY + 1.5;
var row1Y = textY;
var row2Y = row1Y + lineH;
var row3Y = position is null ? row2Y : row2Y + lineH;
var contentBottom = row3Y + lineH;
var pageWidth = sigW + (bgPad * 2);
var pageHeight = contentBottom + bgPad;
var inkBytes = this.EnsureSupportedImageBytes(element.Ink!);
using var processor = new PdfDocumentProcessor();
processor.CreateEmptyDocument();
var page = processor.AddNewPage(new PdfRectangle(0, 0, pageWidth, pageHeight));
using var graphics = processor.CreateGraphics();
using var bgBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 255, 253, 240));
using var linePen = new DXPen(System.Drawing.Color.FromArgb(180, 100, 100, 120), 0.5f);
using var nameBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 20, 20, 20));
using var infoBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 80, 80, 80));
var fontBold = new DXFont("Arial", 7.5f, DXFontStyle.Bold);
var fontNormal = new DXFont("Arial", 6.5f, DXFontStyle.Regular);
graphics.FillRectangle(bgBrush, new System.Drawing.RectangleF(0, 0, (float)pageWidth, (float)pageHeight));
var imageArea = new System.Drawing.RectangleF((float)x, (float)y, (float)sigW, (float)imgH);
var imageRect = FitImageContain(imageArea, inkBytes);
graphics.DrawImage(inkBytes, imageRect);
graphics.DrawLine(linePen, (float)(x + 2), (float)lineY, (float)(x + sigW - 2), (float)lineY);
graphics.DrawString(
element.FullName!.Trim(),
fontBold,
nameBrush,
new System.Drawing.RectangleF((float)(x + padding), (float)row1Y, (float)(sigW - (padding * 2)), (float)lineH));
if (position is not null)
{
graphics.DrawString(
position,
fontNormal,
infoBrush,
new System.Drawing.RectangleF((float)(x + padding), (float)row2Y, (float)(sigW - (padding * 2)), (float)lineH));
}
graphics.DrawString(
$"{element.Place!.Trim()}, {date}",
fontNormal,
infoBrush,
new System.Drawing.RectangleF((float)(x + padding), (float)row3Y, (float)(sigW - (padding * 2)), (float)lineH));
graphics.AddToPageForeground(page, 72, 72);
using var pdfStream = new MemoryStream();
processor.SaveDocument(pdfStream);
pdfStream.Position = 0;
using var rasterProcessor = new PdfDocumentProcessor();
rasterProcessor.LoadDocument(pdfStream);
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(400f);
using DXBitmap bitmap = rasterProcessor.CreateDXBitmap(1, renderOptions);
var pngStream = new MemoryStream();
bitmap.Save(pngStream, DXImageFormat.Png);
pngStream.Position = 0;
return pngStream;
}
private static System.Drawing.RectangleF FitImageContain(System.Drawing.RectangleF area, byte[] imageBytes)
{
using var imageStream = new MemoryStream(imageBytes, writable: false);
using var image = DXImage.FromStream(imageStream);
if (image.Width <= 0 || image.Height <= 0)
return area;
var scale = Math.Min(area.Width / image.Width, area.Height / image.Height);
var drawW = image.Width * scale;
var drawH = image.Height * scale;
var drawX = area.X + ((area.Width - drawW) / 2f);
var drawY = area.Y + ((area.Height - drawH) / 2f);
return new System.Drawing.RectangleF(drawX, drawY, drawW, drawH);
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public bool IsSupportedImageFormat(byte[] bytes)
{
var isPng = bytes.Length >= 4 &&
bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47;
if (isPng)
return true;
var isJpeg = bytes.Length >= 3 &&
bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF;
return isJpeg;
}
}

View File

@@ -0,0 +1,7 @@
namespace EnvelopeGenerator.Infrastructure.Notification.Configuration;
public enum ConsumerType
{
API,
RabbitMQ
}

View File

@@ -0,0 +1,16 @@
using DigitalData.MessagingService.RabbitMQ;
namespace EnvelopeGenerator.Infrastructure.Notification.Configuration;
public class NotificationOptions
{
public const string SectionName = "Notification";
public ConsumerType ConsumerType { get; set; }
public string? ApiUrl { get; set; }
public string SenderEmail { get; set; } = null!;
public RabbitMqConnectionConfiguration? RabbitMQ { get; set; }
}

View File

@@ -0,0 +1,49 @@
using DigitalData.MessagingService.Client;
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
using EnvelopeGenerator.Infrastructure.Notification.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Infrastructure.Notification;
public static class DependencyInjection
{
public static IServiceCollection AddNotificationInfrastructure(this IServiceCollection services, IConfiguration configuration, string sectionName = NotificationOptions.SectionName)
{
var ntfConfiguration = configuration.GetSection(sectionName);
services.Configure<NotificationOptions>(ntfConfiguration);
var options = ntfConfiguration.Get<NotificationOptions>()
?? throw new InvalidOperationException(
$"Failed to bind configuration section '{sectionName}' to {nameof(NotificationOptions)}. Ensure the section exists and is correctly configured.");
switch (options.ConsumerType)
{
case ConsumerType.API:
if (options.ApiUrl is string apiUrl)
services.AddMessagingServiceHttpClient(opt =>
{
opt.BaseAddress = new Uri(apiUrl);
// Additional configuration for the MessagingServiceHttpClient can be added here if needed
});
else
throw new InvalidOperationException(
$"MessagingServiceApiUrl must be provided when ClientType is set to Api in the '{sectionName}' configuration section.");
break;
case ConsumerType.RabbitMQ:
if(options.RabbitMQ is not null)
services.AddMessagingServiceClient(ntfConfiguration, nameof(options.RabbitMQ));
else
throw new InvalidOperationException(
$"Missing RabbitMQ connection configuration." +
$"When ClientType is set to RabbitMQ, a valid RabbitMQ configuration must be provided in the '{sectionName}' configuration section.");
break;
default:
throw new InvalidOperationException(
$"Unsupported ConsumerType '{options.ConsumerType}' in the '{sectionName}' configuration section.");
}
services.AddScoped<IEnvelopeNotificationService, EnvelopeNotificationService>();
return services;
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalData.MessagingService.Client" Version="3.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using DigitalData.MessagingService.Application.Common.Dto;
namespace EnvelopeGenerator.Infrastructure.Notification;
public static class EnvelopeNotificationExtensions
{
public static EmailAttachmentDto ToEmailAttachmentDto(this (string FileName, byte[] Content) attachment) => new()
{
FileName = attachment.FileName,
Content = attachment.Content
};
public static IEnumerable<EmailAttachmentDto> ToEmailAttachmentDto(this IEnumerable<(string FileName, byte[] Content)> attachments)
=> attachments.Select(ToEmailAttachmentDto);
}

View File

@@ -0,0 +1,45 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.EmailSending.Commands;
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Exceptions;
using EnvelopeGenerator.Infrastructure.Notification.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Infrastructure.Notification;
public class EnvelopeNotificationService(IRepository<EmailTemplate> TempRepo, IMessagingServiceClient MsgClient, IOptions<NotificationOptions> ntfOpt) : IEnvelopeNotificationService
{
private readonly NotificationOptions NtfOpt = ntfOpt.Value;
private async Task<EmailTemplate> GetTemplateAsync(EmailTemplateType templateType, CancellationToken cancel = default)
{
return await TempRepo.Where(t => t.Name == templateType.ToString()).FirstOrDefaultAsync(cancel)
?? throw new DataIntegrityException($"{templateType} email template not found");
}
public async Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
where TEnvelopeNotificationBase : EnvelopeNotificationBase
{
var template = await GetTemplateAsync(notification.GetTemplateType(), cancel);
// create email command
var mail = new PublishEmailViaSmtpCommand()
{
Body = template.Body.ReplacePlaceholders(notification),
Subject = template.Subject.ReplacePlaceholders(notification),
Recipients = [receiverEmail],
Sender = NtfOpt.SenderEmail.ToSender(),
IsHtml = true
};
// add attachments if any
if(notification.Attachments?.Any() ?? false)
mail = mail.WithAttachments(notification.Attachments.ToEmailAttachmentDto());
await MsgClient.SendAsync(mail, cancel);
}
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Text.RegularExpressions;
namespace EnvelopeGenerator.Infrastructure.Notification;
public static class ReplacePlaceholderExtensions
{
private sealed record PlaceholderProperty(string Placeholder, PropertyInfo Property);
private static readonly ConcurrentDictionary<Type, PlaceholderProperty[]> PropertyCache = new();
public static string ReplacePlaceholders(this string template, object data)
{
var properties = PropertyCache.GetOrAdd(data.GetType(), CreatePlaceholderProperties);
foreach (var item in properties)
{
var value = item.Property.GetValue(data)?.ToString() ?? string.Empty;
template = template.Replace(
item.Placeholder,
value,
StringComparison.Ordinal);
}
return template;
}
private static PlaceholderProperty[] CreatePlaceholderProperties(
Type type)
{
return [.. type
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
.Select(p => new PlaceholderProperty(
$"[{ToCapitalSnakeCase(p.Name)}]",
p))];
}
private static string ToCapitalSnakeCase(string name)
{
return Regex.Replace(
Regex.Replace(name, @"([A-Z]+)([A-Z][a-z])", "$1_$2"),
@"([a-z0-9])([A-Z])",
"$1_$2").ToUpperInvariant();
}
}

View File

@@ -0,0 +1,17 @@
-- Original email template: DocumentAccessCodeReceived (GUID 5)
-- Backed up before redesign: 2026-10-29 16:32:15
-- Subject: Zugriffscode für Dokument erhalten: ''[DOCUMENT_TITLE]''
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
<br /><B><I>
[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet. <br />
<br />
Verwenden Sie den folgenden Zugriffscode, um das Dokument einzusehen:<br />
<br />
[DOCUMENT_ACCESS_CODE]<br />
<br />
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'
WHERE GUID = 5;

View File

@@ -0,0 +1,15 @@
-- Original email template: DocumentCompleted (GUID 4)
-- Backed up before redesign: 2026-10-29 16:29:55
-- Subject: Umschlag abgeschlossen: ''[DOCUMENT_TITLE]''
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
<br />
Der Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B> wurde erfolgreich abgeschlossen.<br />
<br />
Sie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br />
<br />
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'
WHERE GUID = 4;

View File

@@ -0,0 +1,13 @@
-- Original email template: DocumentDeleted (GUID 2)
-- Backed up before redesign: 2026-10-29 16:23:45
-- Subject: Umschlag zurückgezogen: ''[DOCUMENT_TITLE]''
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
<br /><B><I>
[NAME_SENDER]</I></B> hat den Umschlag <B><I>''[DOCUMENT_TITLE]''</I></B> gelöscht/zurückgezogen.<br /><p>
Begründung: <br /> <I>[REASON]</I> <p>
<br />
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'

View File

@@ -0,0 +1,17 @@
-- Original email template: DocumentReceived (GUID 1, de-DE)
-- Backed up before redesign: 2026-10-29 16:06:07
-- Subject: Dokument erhalten: ''[DOCUMENT_TITLE]''
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
<br /><B><I>
[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet.<br />
<br />
Über den folgenden Link können Sie das Dokument einsehen[DOCUMENT_PROCESS]: <a href="[LINK_TO_DOCUMENT]">[LINK_TO_DOCUMENT_TEXT]</a><br />
<br />
[MESSAGE]<br />
<br />
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'
WHERE GUID = 1;

View File

@@ -0,0 +1,12 @@
-- Original email template: DocumentRejected_ADM (GUID 6)
-- Backed up before redesign: 2026-10-29 16:33:34
-- Subject: ''[DOCUMENT_TITLE]'' - Vorgang zurückgezogen
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_SENDER],<p><B><I>[NAME_RECEIVER]</I></B> hat den Umschlag <B><I>''[DOCUMENT_TITLE]''</I></B> mit folgendem Grund abgelehnt: <p>
[REASON]
<p>Der Umschlag wurde auf den Status Rejected gesetzt. <p>
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'
WHERE GUID = 6;

View File

@@ -0,0 +1,11 @@
-- Original email template: DocumentRejected_REC_2 (GUID 10)
-- Backed up before redesign: 2026-10-29 16:37:12
-- Subject: ''[DOCUMENT_TITLE]'' - Vorgang abgelehnt.
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_RECEIVER],
<p>Der Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B> wurde durch einen anderen Vertragspartner abgelehnt.<p> Der Absender <B><I>[NAME_SENDER]</I></B> wird sich bei Bedarf mit Ihnen in Verbindung setzen. <p>
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'
WHERE GUID = 10;

View File

@@ -0,0 +1,11 @@
-- Original email template: DocumentRejected_REC (GUID 9)
-- Backed up before redesign: 2026-10-29 16:34:21
-- Subject: ''[DOCUMENT_TITLE]'' - Bestätigung der Ablehnung
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_RECEIVER],
<p>Hiermit bestätigen wir Ihnen die Ablehnung des Vorgangs <B><I>''[DOCUMENT_TITLE]''</I></B>!<p>Der Absender <B><I>[NAME_SENDER]</I></B> wurde über die Ablehnung informiert. <p>
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'
WHERE GUID = 9;

View File

@@ -0,0 +1,7 @@
-- Original email template: DocumentShared (GUID 11)
-- Backed up before redesign: 2026-10-29 16:42:51
-- Subject: Dokument geteilt: ''[DOCUMENT_TITLE]''
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag,<br /> <br /><B><I> [NAME_RECEIVER]</I></B> hat Ihnen ein Dokument zum Ansehen gesendet.<br /> <br /> Über den folgenden Link können Sie das Dokument einsehen: <a href="[LINK_TO_DOCUMENT]">[LINK_TO_DOCUMENT_TEXT]</a><br /> <br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]'
WHERE GUID = 11;

View File

@@ -0,0 +1,14 @@
-- Original email template: DocumentSigned (GUID 3)
-- Backed up before redesign: 2026-10-29 16:25:45
-- Subject: Dokument [RECEIVER_ACTION]: ''[DOCUMENT_TITLE]''
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
<br />
hiermit bestätigen wir Ihnen die erfolgreiche [FINAL_STATUS] für den Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B>.<br />
Wenn alle [FINAL_ACTION] haben, erhalten Sie ebenfalls per E-Mail ein abgeschlossenes Exemplar mit entsprechendem Zertifikat!
<br />
Mit freundlichen Grüßen<br />
<br />
[NAME_PORTAL]'
WHERE GUID = 3;

View File

@@ -0,0 +1,8 @@
-- Original email template: TotpSecret (GUID 12)
-- Backed up before redesign: 2026-10-29 16:44:44
-- Subject: 2-Faktor-Authentifizierung QR-Code
-- Restore by running this script against DD_ECM:
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
SET BODY = N'Guten Tag,<br /> <br />Sie können auf Ihren Zwei-Faktor-Authentifizierungscode zugreifen, indem Sie den unten stehenden QR-Code mit einer beliebigen Authentifizierungs-App auf Ihrem Telefon scannen (Google Authenticator, Microsoft Authenticator usw.). Dieser Code ist bis zum [TFA_EXPIRATION] gültig.<br /> <br /> <img src="data:image/png;base64,[TFA_QR_CODE]" style="width: 13rem; height: 13rem;"><br /> <br />
<br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]'
WHERE GUID = 12;

View File

@@ -0,0 +1,7 @@
namespace EnvelopeGenerator.Server.Client.Components.PdfEditor;
public sealed record SenderPdfFieldMoveRequest(
int Index,
int Page,
double XPt,
double YPt);

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