Compare commits

..

43 Commits

Author SHA1 Message Date
a0ed3e2fe4 Add SaveSignatureBehavior and resolve merge conflicts
Introduced the `SaveSignatureBehavior` class as a pipeline behavior for handling `SigningCommand` requests. Added necessary `using` directives to include required namespaces. Resolved merge conflicts in `using` directives between `net8.0` and `net9.0` project versions. Implemented the `Handle` method to delegate request processing.
2026-06-09 23:28:59 +02:00
f5505190e9 Add SignatureDto and mapping for signature handling
Added a new `SignatureDto` record to represent captured signatures with metadata, including properties like `ElementId`, `DataUrl`, `FullName`, `Position`, and `Place`. Updated `SigningCommand` to include a `Signatures` property for handling multiple signatures, deprecating `PsPdfKitAnnotation`.

Introduced a `MappingProfile` class to map `SignatureDto` to `DocReceiverElement` using AutoMapper. Added necessary `using` directives to support the new mapping configuration.
2026-06-09 23:20:53 +02:00
1bfdbac8ff rename Application.Signature directory as DocReceiverElements 2026-06-09 23:06:30 +02:00
4a29511491 Refactor: Replace Signatures with DocReceiverElements
Updated the codebase to replace the `Signature` record with the new `DocReceiverElementCreateDto` record for better alignment with the domain model.

- Updated `EnvelopeReceiverController` to use `DocReceiverElements` instead of `Signatures` when iterating over `Receivers`.
- Replaced the `Signature` record with `DocReceiverElementCreateDto` in `CreateEnvelopeReceiverCommand`.
- Updated `ReceiverGetOrCreateCommand` to use a `DocReceiverElements` property instead of `Signatures`.

These changes ensure consistency and reflect a shift in how document-related data is represented.
2026-06-09 23:03:18 +02:00
1089304bf1 Refactor: Rename SignatureDto to DocReceiverElementDto
Renamed `SignatureDto` to `DocReceiverElementDto` across the codebase to better reflect its purpose as a DTO for document receiver elements.

Updated all references, including:
- `SignatureController.cs`: Changed `doc.Elements` type to `IEnumerable<DocReceiverElementDto>`.
- `DocumentDto.cs`: Updated `Elements` property type.
- `MappingProfile.cs`: Adjusted mappings for the renamed DTO.
- `IDocumentReceiverElementService.cs` and `DocumentReceiverElementService.cs`: Updated interfaces and services to use the new DTO.
- `TestDocumentReceiverElementController.cs`: Updated generic type parameters.

These changes improve clarity, align naming with the domain model, and ensure consistency throughout the application.
2026-06-09 23:00:11 +02:00
9b606a0d3b rename Domain.Interfaces.ISignature as IDocReceiverElement 2026-06-09 22:56:55 +02:00
cb6dea319b rename Entities.Signature as DocReceiverElement 2026-06-09 22:52:41 +02:00
d59aa6157d Rename SignCommand to SigningCommand across codebase
Updated all references to `SignCommand` to use the new `SigningCommand` name for consistency and clarity. This includes changes to class definitions, method signatures, and pipeline behaviors in the following files:

- `DependencyInjection.cs`: Updated pipeline behaviors to use `SigningCommand`.
- `AnnotationBehavior.cs`: Updated class definition and methods to use `SigningCommand`. Marked `SignCommand` as `[Obsolete]`.
- `DocStatusBehavior.cs`, `EnvelopeReceiverResolutionBehavior.cs`, `HistoryBehavior.cs`, `SendSignedMailBehavior.cs`: Updated class definitions and methods to use `SigningCommand`.
- `SendSignedMailBehavior.cs`: Updated `CreatePlaceHolders` method to accept `SigningCommand`.
- `SigningCommand.cs`: Renamed `SignCommand` record to `SigningCommand` and updated internal methods and properties. Renamed `SignCommandHandler` to `SigningCommandHandler`.

Marked `SignCommand` as `[Obsolete]` where applicable to indicate deprecation. These changes improve code readability and align the command name with its purpose in the signing process.
2026-06-09 22:47:08 +02:00
1569647b60 Update pipeline behaviors for SignCommand in MediatR
Added EnvelopeReceiverResolutionBehavior as the first pipeline
behavior for SignCommand. Updated execution order of existing
behaviors: AnnotationBehavior (2nd), DocStatusBehavior (3rd),
and HistoryBehavior (4th). SendSignedMailBehavior remains last.
Updated comments to reflect the new execution order.
2026-06-09 22:43:37 +02:00
3bb2a013ab Add EnvelopeReceiverResolutionBehavior pipeline behavior
Introduced a new pipeline behavior class `EnvelopeReceiverResolutionBehavior` to resolve and validate the `EnvelopeReceiver` during the signing process.

- Added necessary `using` directives for dependencies such as `AutoMapper`, `MediatR`, and `IRepository`.
- Implemented the `Handle` method to query the database for `EnvelopeReceiver` if not provided in the `SignCommand` request.
- Throws a `NotFoundException` if the `EnvelopeReceiver` is not found.
- Maps the retrieved entity to a DTO and sets it in the request.
- Ensures the behavior executes before other signing process behaviors.
2026-06-09 22:43:19 +02:00
215b755f92 Refactor SignCommand and improve handler comments
Refactored the `SignCommand` class to inherit from `EnvelopeReceiverQueryBase` and introduced a private backing field `_envelopeReceiver` for better encapsulation. Added an internal method `SetEnvelopeReceiver` to manage the envelope receiver data. Updated the `EnvelopeReceiver` property to use the backing field and removed the `required` modifier for more controlled initialization.

Clarified comments in `SignCommandHandler` to emphasize that all processing is handled by pipeline behaviors, leaving the handler intentionally empty. Made minor adjustments to comments for improved clarity and consistency.
2026-06-09 22:42:50 +02:00
3a94733047 Refactor SignCommand and deprecate PsPdfKitAnnotation
Removed the `EmailAddress` property from the `SignCommand` class, which previously retrieved the receiver's email address and threw an exception if the receiver was null. This change eliminates reliance on `EnvelopeReceiver`.

Removed the `ToJson` extension method usage and the associated `using EnvelopeGenerator.Application.Common.Extensions;` directive, as well as the unused `using EnvelopeGenerator.Domain.Constants;` directive.

Marked the `PsPdfKitAnnotation` property as `[Obsolete]`, directing users to use `Signature.Commands.SignCommand` instead, signaling a transition to a newer implementation.
2026-06-09 19:11:55 +02:00
7793d3cbb9 Update EmailOut to use EnvelopeReceiver properties
Updated the `EmailAddress` and `ReferenceString` properties
of the `EmailOut` object in `SendSignedMailBehavior` to use
`request.EnvelopeReceiver.Receiver!.EmailAddress` instead of
`request.EmailAddress`. This ensures the values are derived
from the `EnvelopeReceiver.Receiver` object for improved
accuracy and consistency.
2026-06-09 19:11:44 +02:00
9174065365 Pass cancellationToken to next() in pipeline behaviors
Updated the `Handle` method in multiple classes implementing
`IPipelineBehavior<SignCommand, Unit>` to pass the `cancellationToken`
parameter to the `next()` method. This change ensures consistent
propagation of the `cancellationToken` through the pipeline, enabling
proper handling of cancellation requests during asynchronous operations.

Modified files:
- AnnotationBehavior.cs
- DocStatusBehavior.cs
- HistoryBehavior.cs
- SendSignedMailBehavior.cs
2026-06-09 19:07:38 +02:00
19824afc1c Mark PsPdfKitAnnotation as obsolete; remove TemplateType
The `PsPdfKitAnnotation` property in the `SignCommand` class has been marked as `[Obsolete]` with a deprecation message suggesting the use of `Signature.Commands.SignCommand` instead.

The `TemplateType` property, which returned the `EmailTemplateType.DocumentSigned` value, has been removed from the `SignCommand` class.
2026-06-09 19:07:26 +02:00
c7fe3f0b9c Mark PsPdfKitAnnotation as obsolete
Added an [Obsolete] attribute to the PsPdfKitAnnotation property
in the DocSignedNotification class, indicating that the PSPDFKit
library is deprecated.

Reorganized using directives in DocumentReceiverElementService.cs
to improve consistency and maintain code readability.
2026-06-09 19:04:55 +02:00
a98024063a Refactor namespaces for SignCommand and behaviors
Updated namespaces from `Signature` to `Signatures` for consistency and clarity across the application. Simplified pipeline behavior registrations in `DependencyInjection.cs` by using shorter references. Added `Microsoft.Extensions.Configuration` to `DependencyInjection.cs` to support configuration functionality. Ensured all references to `SignCommand` and related behaviors align with the new namespace structure.
2026-06-09 18:55:31 +02:00
e0cab3f965 Mark PSPDFKit-related code as obsolete
Added `[Obsolete]` attributes to classes, methods, and properties related to the deprecated PSPDFKit library and notifications.

- Marked `AnnotationHandler`, `DocStatusHandler`, `AnnotationBehavior`, and `DocStatusBehavior` as obsolete.
- Marked `Handle` methods in `DocStatusHandler` and `DocStatusBehavior` as obsolete.
- Marked `PsPdfKitAnnotation` property in `SignCommand` as obsolete.
- Marked `CreateOrUpdate` method in `AnnotationController` as obsolete.
- Added `Handle` methods in `DocStatusHandler` and `DocStatusBehavior` to send `CreateDocStatusCommand`.
- Updated `AnnotationController` dependencies to include `EnvelopeGenerator.Application.Common.Dto`.

These changes indicate a transition to `Signature.Commands.SignCommand` and deprecate PSPDFKit-related functionality.
2026-06-09 18:45:51 +02:00
e5347b063d Add pipeline behaviors for signing process
Introduced four new pipeline behaviors (`AnnotationBehavior`,
`DocStatusBehavior`, `HistoryBehavior`, and `SendSignedMailBehavior`)
to modularize the signing process. These behaviors handle annotation
persistence, document status creation, history recording, and signed
mail notifications, respectively.

- `AnnotationBehavior`: Saves annotations using `IRepository`.
- `DocStatusBehavior`: Creates document status via `ISender`.
- `HistoryBehavior`: Records signing history and validates receiver
  information.
- `SendSignedMailBehavior`: Sends signed mail notifications using
  email templates and dynamic placeholder replacement.

Added error handling for missing data in `HistoryBehavior` and
`SendSignedMailBehavior`. Updated the signing pipeline to execute
these behaviors sequentially, ensuring a structured and extensible
workflow.
2026-06-09 18:33:57 +02:00
ecd695ad37 Add MediatR pipeline behaviors for SignCommand
Introduced MediatR to the project by adding the `using MediatR;` directive in `DependencyInjection.cs`. Registered pipeline behaviors for the `Signature.Commands.SignCommand` to enforce a structured execution order:

1. AnnotationBehavior: Saves annotations.
2. DocStatusBehavior: Creates document status.
3. HistoryBehavior: Records history.
4. SendSignedMailBehavior: Sends notification email (executes last).

These changes improve the command handling pipeline by ensuring sequential and reliable execution of behaviors.
2026-06-09 18:33:27 +02:00
5a8809ffc1 Add new dependencies to DocSignedNotificationTests.cs
Added `using` directives for namespaces related to DTOs,
envelope receivers, and document signing notifications.
These changes enable the use of new functionality or
tests for document signing notifications.
2026-06-09 18:25:01 +02:00
b832637a6a Update AnnotationController with new dependencies
Added `using` directives for additional namespaces to support
application DTOs, extensions, services, notifications, and
envelope receiver queries. These changes introduce new
functionality and dependencies to the `AnnotationController`
class.
2026-06-09 18:23:08 +02:00
fceb266630 Deprecate DocSignedNotification and cleanup annotations
Marked `DocSignedNotification` as `[Obsolete]` with a note to use `Signature.Commands.SignCommand` instead. Removed the `PsPdfKitAnnotation` record and its associated `using` directives from `DocSignedNotification.cs`.

Added missing `using EnvelopeGenerator.Application.Common.Dto;` to `AnnotationHandler.cs` and `DocStatusHandler.cs` to ensure proper DTO support.
2026-06-09 18:22:43 +02:00
87bdef9d5e Add SignCommand and handler for document signing
Introduce `SignCommand` and `SignCommandHandler` in the `EnvelopeGenerator.Application.Signature.Commands` namespace.

- `SignCommand` is a CQRS command that encapsulates the envelope receiver's information (`EnvelopeReceiverDto`) and optional PSPDFKit annotation data.
- Added computed properties for email template type (`TemplateType`) and receiver's email address (`EmailAddress`), with validation for null receivers.
- `SignCommandHandler` implements the `IRequestHandler<SignCommand>` interface, with a placeholder `Handle` method delegating processing to pipeline behaviors.
- Added necessary `using` directives for MediatR, DTOs, extensions, and constants.
2026-06-09 18:12:12 +02:00
2fb32fb982 Add PsPdfKitAnnotation record with deprecation notice
Introduced a new `PsPdfKitAnnotation` record in the namespace
`EnvelopeGenerator.Application.Common.Dto` to represent PSPDFKit
annotation data. The record includes two parameters: `Instant`
of type `ExpandoObject` and `Structured` of type
`IEnumerable<AnnotationCreateDto>`.

Marked the record with the `[Obsolete]` attribute to indicate
that the PSPDFKit library is deprecated. Added a summary XML
comment to describe the purpose of the record. Also added a
`using` directive for `System.Dynamic` to support `ExpandoObject`.
2026-06-09 18:07:27 +02:00
63d050244c Rename method in SignatureController to Get
Renamed the method `GetAnnotsOfReceiver` to `Get` in the
`SignatureController` class. This change simplifies the method
name, making it more generic and potentially aligning with
naming conventions or broader use cases.
2026-06-09 17:05:43 +02:00
126a4acb12 Add new properties to SignatureDto for signature details
Added the `System.ComponentModel.DataAnnotations.Schema` namespace.
Introduced new properties to the `SignatureDto` class:
- `FullName` (string?) for the full name of the signature entity.
- `Position` (string?) for the position of the signature entity.
- `Place` (string?) for the place associated with the signature entity.
- `Ink` (byte[]?) for the ink data of the signature entity.
2026-06-09 17:05:23 +02:00
082cb322ef Refactor comments for clarity and consistency
Replaced non-English comments with English equivalents across
`EnvelopeViewer.razor` and `pdf-viewer.js` to improve code
readability and maintainability. Updated comments to clarify
the purpose of variables, methods, and DOM manipulation logic
without altering functionality. These changes ensure the
codebase is more accessible to a broader audience.
2026-06-09 17:05:01 +02:00
cd077aa1bd Refactor DocSignedNotification for better encapsulation
Refactored `DocSignedNotification` to remove inheritance from
`EnvelopeReceiverDto` and introduced a required `EnvelopeReceiver`
property. Updated all usages across the codebase to align with the
new structure, including controllers, handlers, and tests.

- Improved encapsulation and reduced coupling by making
  dependencies explicit.
- Updated `AnnotationController`, `DocStatusHandler`,
  `HistoryHandler`, and `SendSignedMailHandler` to use the
  `EnvelopeReceiver` property.
- Adjusted `DocSignedNotificationTests` to reflect the new
  instantiation pattern.
- Updated XML documentation and ensured consistent access to
  `EnvelopeReceiver` properties like `EnvelopeId`, `ReceiverId`,
  and `EmailAddress`.
2026-06-09 16:34:49 +02:00
49ac35153e Refactor AnnotationController and DocSignedNotification
Refactored `AnnotationController` to simplify `DocSignedNotification` creation and improve error handling. Replaced the `ToDocSignedNotification` extension method with direct instantiation of `DocSignedNotification`. Introduced a `try-catch` block to handle exceptions during notification publishing, ensuring a `RemoveSignatureNotification` is sent on failure.

Removed `ToDocSignedNotification` and `PublishSafely` extension methods, as their functionality was inlined into the controller. Updated tests to reflect these changes. Simplified the `DocSignedNotification` class by removing redundant methods.

Improved maintainability and clarity by reducing dependencies on extension methods and handling exceptions explicitly.
2026-06-09 15:39:41 +02:00
91a563d995 Refactor SQL cache configuration in Program.cs
Simplified the configuration of the distributed SQL Server cache by using the `Bind` method to map all properties from the `Cache:SqlServer` configuration section directly to the `options` object.

Added a fallback to ensure the `ConnectionString` is set to `connStr` if it is empty or whitespace.
2026-06-09 14:57:48 +02:00
308cdd03f2 Add SQL Server distributed cache configuration
Configured the application to use SQL Server as a distributed
cache provider. Added `AddDistributedSqlServerCache` to
`Program.cs` and set up the connection string, schema name,
and table name from the `Cache:SqlServer` configuration
section. This enables persistent and shared caching across
multiple application instances.
2026-06-09 13:52:30 +02:00
f35068e368 Add conditional SQL Server caching for .NET 8.0
Added a new package reference for `Microsoft.Extensions.Caching.SqlServer` version `8.0.11`, conditionally included for the `net8.0` target framework. This enables SQL Server-based caching functionality for applications targeting .NET 8.0.
2026-06-09 13:52:15 +02:00
e490805025 Add SQL Server cache configuration to appsettings.json
Added a "SqlServer" object under the "Cache" section in
`appsettings.json` with properties for "ConnectionString",
"SchemaName" (set to "dbo"), and "TableName" (set to "TBDD_CACHE").
No changes were made to the "SignatureCacheExpiration" property,
but it was re-added in the diff for context.
2026-06-09 13:52:03 +02:00
08550f87e6 Refactor ReceiverSignature to method in CacheController
Updated `ReceiverSignature` usage in `CacheController` to reflect its refactoring from a property to a method. This change was applied consistently across the `SaveSignature`, `GetSignature`, and `DeleteSignature` methods, ensuring the `cacheKey` variable now uses `User.ReceiverSignature()` instead of `User.ReceiverSignature`. This refactor likely accommodates additional logic in the `ReceiverSignature` method.
2026-06-09 13:32:38 +02:00
a22ec7a7d3 Enhance signature management functionality
Added a new button in `EnvelopeViewer.razor` for creating or modifying signatures, with dynamic styling and tooltips based on the signature state. Enhanced `OpenSignaturePopup` and `OnPopupShownAsync` methods to preload and display existing signatures in the popup and canvas.

Introduced new "success" button styles in `envelope-viewer.css` for better visual feedback. Added `loadExistingSignature` function in `receiver-signature.js` to render existing signatures on the canvas and updated the public API to expose this functionality.
2026-06-09 11:55:56 +02:00
f4681f85e7 Add signature caching and logging to EnvelopeViewer
Introduced `SignatureCacheService` and `ILogger<EnvelopeViewer>` to enable caching and logging functionality. Added logic to load cached signatures when available, bypassing the signature popup. Implemented asynchronous, fire-and-forget caching of captured signatures, with error handling to ignore cache failures. Updated signature handling to integrate with the caching mechanism, improving user experience and performance.
2026-06-09 11:14:49 +02:00
0ed4a44df0 Refactor services and add configuration options
Replaced scoped services from the `EnvelopeGenerator.ReceiverUI.Services` namespace with their counterparts without the namespace prefix. Added a new `SignatureCacheService` and updated `AppVersionService` to use the non-namespaced version.

Added `builder.RootComponents.Add<HeadOutlet>("head::after");` to register a root component. Introduced configuration binding for `ApiOptions` and `PdfViewerOptions`.

DevExpress components for reporting and PDF viewing remain unchanged, with additional configuration for `DevExpressBlazorReportingWebAssembly` included.
2026-06-09 11:11:16 +02:00
b8926f25c4 Update CacheController and SignatureCacheRequest
- Added `using EnvelopeGenerator.API.Extensions;` for utilities.
- Changed `SignatureCacheKeyPrefix` to a new prefix value.
- Added `[Authorize(Policy = AuthPolicy.Receiver)]` to methods.
- Used `[FromRoute]` for `envelopeKey` in route-bound methods.
- Updated cache key logic to use `User.ReceiverSignature`.
- Made `DataUrl`, `FullName`, and `Place` required in `SignatureCacheRequest`.
- Set default value (`null`) for `Position` in `SignatureCacheRequest`.
2026-06-09 10:59:13 +02:00
5b220932d3 Refactor claim handling and simplify controllers
Refactored multiple controllers (`AnnotationController`,
`DocumentController`, `ReadOnlyController`, and
`SignatureController`) to use updated claim extension methods
(`ReceiverSignature`, `EnvelopeUuid`, etc.), replacing older,
verbose methods for improved readability and consistency.

Removed the `EnvelopeClaimTypes` class and replaced claim type
constants with `EnvelopeClaimNames`. Simplified claim retrieval
logic in `ReceiverClaimExtensions` by consolidating methods and
removing redundant or unused functionality.

Eliminated the `SignInEnvelopeAsync` method, indicating a shift
away from manual claim management. Performed general cleanup,
including removing obsolete code and improving exception
messages for better debugging context.
2026-06-09 10:54:38 +02:00
50c02314ef Add SignatureCacheService for managing cached signatures
Introduced a new `SignatureCacheService` class to handle
cached signatures via API interactions. This includes methods
for saving, retrieving, and deleting signatures using `HttpClient`.

- Added dependency injection for `HttpClient` and `IOptions<ApiOptions>`.
- Implemented `SaveSignatureAsync`, `GetSignatureAsync`, and
  `DeleteSignatureAsync` methods with error handling.
- Utilized `Uri.EscapeDataString` for safe URL encoding.
- Added support for HTTP operations with `System.Net.Http.Json`.
2026-06-09 09:50:50 +02:00
223bb88f54 Add CacheController and caching support for signatures
Introduced a new `CacheController` to manage cached data for
receiver signatures using distributed caching. Added endpoints
to save, retrieve, and delete cached signatures.

Created a `SignatureCacheRequest` model for caching payloads
and a `CacheOptions` class to configure cache settings,
including optional expiration. Updated `Program.cs` to bind
`CacheOptions` to the `Cache` section in `appsettings.json`.

Added a new `Cache` section in `appsettings.json` with a
`SignatureCacheExpiration` property, defaulting to `null`
(no expiration).
2026-06-08 17:14:08 +02:00
38f4da00da Add new properties to Signature class for database mapping
Added four new properties (`FullName`, `Position`, `Place`, and
`Ink`) to the `Signature` class. Each property is annotated with
the `[Column]` attribute to map to corresponding database columns
(`FULL_NAME`, `POSITION`, `PLACE`, and `INK`). The properties
support nullable values based on the `nullable` directive,
ensuring compatibility with nullable reference types. These
changes enhance the `Signature` class by enabling additional
data storage and retrieval.
2026-06-08 17:13:22 +02:00
57 changed files with 1388 additions and 291 deletions

View File

@@ -935,6 +935,229 @@ canvas {
--- ---
## Signature Caching System — Session 15
**Purpose:** Persist receiver signature across page refreshes using distributed cache (Redis/SQL Server).
### Architecture
**API Cache Controller (`EnvelopeGenerator.API/Controllers/CacheController.cs`):**
```csharp
[Route("api/[controller]")]
[Authorize(Policy = AuthPolicy.Receiver)]
public class CacheController(IDistributedCache cache, IOptions<CacheOptions> cacheOptions) : ControllerBase
{
private const string SignatureCacheKeyPrefix = "signature:91751687-8ae6-4777-bf5f-b8846085e62e:";
[HttpPost("SignatureCapture/{envelopeKey}")]
public async Task<IActionResult> SaveSignature(string envelopeKey,
[FromBody] SignatureCaptureDto request, CancellationToken cancel)
[HttpGet("SignatureCapture/{envelopeKey}")]
public async Task<IActionResult> GetSignature(string envelopeKey, CancellationToken cancel)
[HttpDelete("SignatureCapture/{envelopeKey}")]
public async Task<IActionResult> DeleteSignature(string envelopeKey, CancellationToken cancel)
}
```
**Cache Options (`EnvelopeGenerator.API/Options/CacheOptions.cs`):**
```csharp
public sealed class CacheOptions
{
public const string SectionName = "Cache";
// If null, signatures never expire (until manual delete)
public TimeSpan? SignatureCacheExpiration { get; set; }
}
```
**Configuration (appsettings.json):**
```json
{
"Cache": {
"SignatureCacheExpiration": null // Or "02:00:00" for 2 hours
}
}
```
**Blazor Service (`EnvelopeGenerator.ReceiverUI/Services/SignatureCacheService.cs`):**
```csharp
public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOptions)
{
public async Task SaveSignatureAsync(string envelopeKey,
SignatureCaptureDto signature, CancellationToken cancel = default)
public async Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey,
CancellationToken cancel = default)
public async Task DeleteSignatureAsync(string envelopeKey,
CancellationToken cancel = default)
}
```
### Workflow
**1. Page Load (First Time):**
```csharp
protected override async Task OnInitializedAsync() {
// Try to load cached signature first
try {
var cachedSignature = await SignatureCacheService.GetSignatureAsync(EnvelopeKey);
if (cachedSignature is not null) {
_capturedSignature = cachedSignature;
_signerFullName = cachedSignature.FullName;
_signerPosition = cachedSignature.Position;
_signaturePlace = cachedSignature.Place;
_signaturePopupVisible = false; // Skip popup
} else {
// No cache - show popup
_signaturePopupVisible = true;
}
} catch (Exception ex) {
logger.LogWarning(ex, "Failed to load cached signature, showing popup");
_signaturePopupVisible = true; // Fallback to popup
}
}
```
**2. Save Signature:**
```csharp
async Task SaveSignatureAsync() {
// ... validation ...
_capturedSignature = new SignatureCaptureDto { ... };
_signaturePopupVisible = false;
// Save to cache (fire-and-forget, ignore errors)
_ = Task.Run(async () => {
try {
await SignatureCacheService.SaveSignatureAsync(EnvelopeKey, _capturedSignature);
} catch {
// Ignore cache errors
}
});
}
```
**3. Change Signature (Toolbar Button):**
```csharp
void OpenSignaturePopup() {
_activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = true;
// Load current signature into form fields
if (_capturedSignature is not null) {
_signerFullName = _capturedSignature.FullName;
_signerPosition = _capturedSignature.Position;
_signaturePlace = _capturedSignature.Place;
}
}
async Task OnPopupShownAsync() {
await InitializeActiveSignatureTabAsync();
// Load existing signature image to canvas (Draw tab)
if (_capturedSignature is not null && _activeSignatureTab == SignatureTabDraw) {
await Task.Delay(100);
await JSRuntime.InvokeVoidAsync("receiverSignature.loadExistingSignature",
DrawCanvasId, _capturedSignature.DataUrl);
}
}
```
**JavaScript Helper:**
```javascript
receiverSignature.loadExistingSignature(canvasId, dataUrl) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
state.hasSignature = true;
};
img.src = dataUrl;
}
```
**4. Restart Signing (Reset Button):**
```csharp
void RestartSigning() {
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
// Page reload ? cache cleared ? popup shows
}
```
### Toolbar Button Design
**Change Signature Button:**
```html
<button class="pdf-toolbar__btn @(_capturedSignature is not null ? "pdf-toolbar__btn--success" : "")"
@onclick="OpenSignaturePopup"
title="@(_capturedSignature is not null ? "Unterschrift ändern" : "Unterschrift erstellen")">
@if (_capturedSignature is not null) {
<svg>?</svg> <!-- Checkmark icon -->
} else {
<svg>??</svg> <!-- Pen icon -->
}
</button>
```
**CSS:**
```css
.pdf-toolbar__btn--success {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
border-color: rgba(16, 185, 129, 0.3);
color: #059669;
}
.pdf-toolbar__btn--success:hover:not(:disabled) {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
```
**States:**
- **No Signature:** Blue button with pen icon
- **Signature Created:** Green button with checkmark icon
- **Hover:** Filled green gradient with white icon
### Cache Key Format
```
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
```
- Prefix prevents collisions with other cache keys
- GUID ensures uniqueness across application instances
- `envelopeKey` is user-provided identifier (URL parameter)
### Error Handling Philosophy
**API Controller:** No validation, no try-catch, no logging
- Throws exceptions directly to caller
- Blazor handles errors with try-catch
**Blazor Service:** No try-catch
- Throws `HttpRequestException` with status code + body
- Component catches and handles
**Blazor Component:** Try-catch with fallback
- Graceful degradation: show popup on cache failure
- Fire-and-forget saves: ignore errors
### Benefits
1. **UX:** No need to re-enter signature on page refresh
2. **Performance:** Fast cache retrieval (Redis/SQL)
3. **Security:** Per-receiver isolation (cookie-based auth)
4. **Flexibility:** Configurable expiration time
5. **Reliability:** Graceful degradation on cache failure
---

View File

@@ -5,7 +5,7 @@ using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services; using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned; using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.Documents.Queries; using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Histories.Queries; using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
@@ -13,7 +13,6 @@ using MediatR;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.API.Controllers; namespace EnvelopeGenerator.API.Controllers;
@@ -62,8 +61,8 @@ public class AnnotationController : ControllerBase
[Obsolete("PSPDF Kit will no longer be used.")] [Obsolete("PSPDF Kit will no longer be used.")]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default) public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{ {
var signature = User.GetReceiverSignatureOfReceiver(); var signature = User.ReceiverSignature();
var uuid = User.GetEnvelopeUuidOfReceiver(); var uuid = User.EnvelopeUuid();
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound); var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
@@ -75,12 +74,24 @@ public class AnnotationController : ControllerBase
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel)) else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
return Problem(statusCode: StatusCodes.Status423Locked); return Problem(statusCode: StatusCodes.Status423Locked);
var docSignedNotification = await _mediator var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
.ReadEnvelopeReceiverAsync(uuid, signature, cancel) var docSignedNotification = envelopeReceiverDto is not null
.ToDocSignedNotification(psPdfKitAnnotation) ? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
?? throw new NotFoundException("Envelope receiver is not found."); : throw new NotFoundException("Envelope receiver is not found.");
await _mediator.PublishSafely(docSignedNotification, cancel); try
{
await _mediator.Publish(docSignedNotification, cancel);
}
catch (Exception)
{
await _mediator.Publish(new RemoveSignatureNotification()
{
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
}, cancel);
throw;
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok(); return Ok();
@@ -95,9 +106,9 @@ public class AnnotationController : ControllerBase
[Obsolete("Use MediatR")] [Obsolete("Use MediatR")]
public async Task<IActionResult> Reject([FromBody] string? reason = null) public async Task<IActionResult> Reject([FromBody] string? reason = null)
{ {
var signature = User.GetReceiverSignatureOfReceiver(); var signature = User.ReceiverSignature();
var uuid = User.GetEnvelopeUuidOfReceiver(); var uuid = User.EnvelopeUuid();
var mail = User.GetReceiverMailOfReceiver(); var mail = User.ReceiverMail();
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature); var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);

View File

@@ -0,0 +1,84 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using System.Text.Json;
using EnvelopeGenerator.API.Options;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.API.Extensions;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Manages cached data for receivers using distributed cache.
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = AuthPolicy.Receiver)]
public class CacheController(
IDistributedCache cache,
IOptions<CacheOptions> cacheOptions) : ControllerBase
{
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
/// <summary>
/// Stores a receiver's signature in cache for the specified envelope.
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpPost("SignatureCapture/{envelopeKey}")]
public async Task<IActionResult> SaveSignature(
[FromRoute] string envelopeKey,
[FromBody] SignatureCacheRequest request,
CancellationToken cancel)
{
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
var json = JsonSerializer.Serialize(request);
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
: null;
await cache.SetStringAsync(cacheKey, json, options ?? new DistributedCacheEntryOptions(), cancel);
return Ok();
}
/// <summary>
/// Retrieves a cached signature for the specified envelope.
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("SignatureCapture/{envelopeKey}")]
public async Task<IActionResult> GetSignature([FromRoute] string envelopeKey, CancellationToken cancel)
{
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
var json = await cache.GetStringAsync(cacheKey, cancel);
if (json is null)
return NotFound();
var signature = JsonSerializer.Deserialize<SignatureCacheRequest>(json);
return Ok(signature);
}
/// <summary>
/// Deletes a cached signature for the specified envelope.
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpDelete("SignatureCapture/{envelopeKey}")]
public async Task<IActionResult> DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel)
{
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
await cache.RemoveAsync(cacheKey, cancel);
return Ok();
}
}
/// <summary>
/// Request model for caching signature data.
/// </summary>
public sealed record SignatureCacheRequest(
string DataUrl,
string FullName,
string Place,
string? Position = null);

View File

@@ -51,7 +51,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
if (query is not null) if (query is not null)
return BadRequest("Query parameters are not allowed for receiver role."); return BadRequest("Query parameters are not allowed for receiver role.");
var envelopeId = User.GetEnvelopeIdOfReceiver(); var envelopeId = User.EnvelopeId();
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel); var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
return receiverDoc.ByteData is byte[] receiverDocByte return receiverDoc.ByteData is byte[] receiverDocByte
? File(receiverDocByte, "application/octet-stream") ? File(receiverDocByte, "application/octet-stream")
@@ -71,7 +71,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
[HttpGet("{envelopeKey}")] [HttpGet("{envelopeKey}")]
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel) public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
{ {
int envelopeId = User.GetEnvelopeIdOfReceiver(); int envelopeId = User.EnvelopeId();
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel); var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);

View File

@@ -200,7 +200,7 @@ public class EnvelopeReceiverController : ControllerBase
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];"; SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
foreach (var rcv in res.SentReceiver) foreach (var rcv in res.SentReceiver)
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.Signatures ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.Signature>()) foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.DocReceiverElementCreateDto>())
{ {
using SqlConnection conn = new(_cnnStr); using SqlConnection conn = new(_cnnStr);
conn.Open(); conn.Open();

View File

@@ -41,14 +41,14 @@ public class ReadOnlyController : ControllerBase
[Obsolete("Use MediatR")] [Obsolete("Use MediatR")]
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto) public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
{ {
var authReceiverMail = User.GetReceiverMailOfReceiver(); var authReceiverMail = User.ReceiverMail();
if (authReceiverMail is null) if (authReceiverMail is null)
{ {
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto)); _logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
return Unauthorized(); return Unauthorized();
} }
var envelopeId = User.GetEnvelopeIdOfReceiver(); var envelopeId = User.EnvelopeId();
createDto.AddedWho = authReceiverMail; createDto.AddedWho = authReceiverMail;
createDto.EnvelopeId = envelopeId; createDto.EnvelopeId = envelopeId;

View File

@@ -36,15 +36,15 @@ public class SignatureController : ControllerBase
/// <returns></returns> /// <returns></returns>
[Authorize(Policy = AuthPolicy.Receiver)] [Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("{envelopeKey}")] [HttpGet("{envelopeKey}")]
public async Task<IActionResult> GetAnnotsOfReceiver(string envelopeKey, CancellationToken cancel) public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
{ {
int envelopeId = User.GetEnvelopeIdOfReceiver(); int envelopeId = User.EnvelopeId();
int receiverId = User.GetReceiverIdOfReceiver(); int receiverId = User.ReceiverId();
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel); var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
if (doc.Elements is not IEnumerable<SignatureDto> docSignatures) if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
return NotFound("Document is empty."); return NotFound("Document is empty.");
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList(); var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();

View File

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

View File

@@ -34,6 +34,7 @@
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" /> <PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" /> <PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" /> <PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="itext" Version="8.0.5" /> <PackageReference Include="itext" Version="8.0.5" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" /> <PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />

View File

@@ -1,8 +1,6 @@
using System.Linq; using DigitalData.Auth.Claims;
using Microsoft.IdentityModel.JsonWebTokens;
using System.Security.Claims; using System.Security.Claims;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace EnvelopeGenerator.API.Extensions; namespace EnvelopeGenerator.API.Extensions;
@@ -11,12 +9,14 @@ namespace EnvelopeGenerator.API.Extensions;
/// </summary> /// </summary>
public static class ReceiverClaimExtensions public static class ReceiverClaimExtensions
{ {
private static readonly string[] EnvelopeIdClaimTypes = [EnvelopeClaimTypes.Id, "envelope_id", "EnvelopeId"]; /// <summary>
private static readonly string[] ReceiverIdClaimTypes = ["receiver_id", "ReceiverId"]; ///
private static readonly string[] EnvelopeUuidClaimTypes = [ClaimTypes.NameIdentifier, "envelope_uuid", "EnvelopeUuid"]; /// </summary>
private static readonly string[] ReceiverSignatureClaimTypes = [ClaimTypes.Hash, "receiver_sig", "ReceiverSignature"]; /// <param name="user"></param>
/// <param name="claimType"></param>
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, string claimType) /// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
private static string GetRequiredClaimValue(this ClaimsPrincipal user, string claimType)
{ {
var value = user.FindFirstValue(claimType); var value = user.FindFirstValue(claimType);
if (value is not null) if (value is not null)
@@ -32,7 +32,7 @@ public static class ReceiverClaimExtensions
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, params string[] claimTypes) private static string GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes)
{ {
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct()) foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
{ {
@@ -52,89 +52,45 @@ public static class ReceiverClaimExtensions
/// <summary> /// <summary>
/// Gets the authenticated envelope UUID from the claims. /// Gets the authenticated envelope UUID from the claims.
/// </summary> /// </summary>
public static string GetEnvelopeUuidOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeUuidClaimTypes); public static string EnvelopeUuid(this ClaimsPrincipal user)
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
/// <summary> /// <summary>
/// Gets the authenticated receiver signature from the claims. /// Gets the authenticated receiver signature from the claims.
/// </summary> /// </summary>
public static string GetReceiverSignatureOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ReceiverSignatureClaimTypes); public static string ReceiverSignature(this ClaimsPrincipal user)
=> user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature);
/// <summary>
/// Gets the authenticated receiver display name from the claims.
/// </summary>
public static string GetReceiverNameOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Name);
/// <summary> /// <summary>
/// Gets the authenticated receiver email address from the claims. /// Gets the authenticated receiver email address from the claims.
/// </summary> /// </summary>
public static string GetReceiverMailOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Email); public static string ReceiverMail(this ClaimsPrincipal user)
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
/// <summary>
/// Gets the authenticated envelope title from the claims.
/// </summary>
public static string GetEnvelopeTitleOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Title);
/// <summary> /// <summary>
/// Gets the authenticated envelope identifier from the claims. /// Gets the authenticated envelope identifier from the claims.
/// </summary> /// </summary>
public static int GetEnvelopeIdOfReceiver(this ClaimsPrincipal user) public static int EnvelopeId(this ClaimsPrincipal user)
{ {
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeIdClaimTypes); var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId);
if (!int.TryParse(envIdStr, out var envId)) if (int.TryParse(envIdStr, out var envId))
{ return envId;
throw new InvalidOperationException($"Claim '{"envelope_id"}' is not a valid integer."); else
} throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer.");
return envId;
} }
/// <summary> /// <summary>
/// /// Gets the authenticated receiver identifier from the claims.
/// </summary> /// </summary>
/// <param name="user"></param> /// <param name="user"></param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>
public static int GetReceiverIdOfReceiver(this ClaimsPrincipal user) public static int ReceiverId(this ClaimsPrincipal user)
{ {
var rcvIdStr = user.GetRequiredClaimOfReceiver(ReceiverIdClaimTypes); var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
if (!int.TryParse(rcvIdStr, out var rcvId)) if (int.TryParse(rcvIdStr, out var rcvId))
{ return rcvId;
throw new InvalidOperationException($"Claim '{"receiver_id"}' is not a valid integer."); else
} throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
return rcvId;
}
/// <summary>
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
/// </summary>
/// <param name="context">The current HTTP context.</param>
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
new(ClaimTypes.Role, receiverRole)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = false,
IsPersistent = false
};
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
} }
} }

View File

@@ -0,0 +1,18 @@
namespace EnvelopeGenerator.API.Options;
/// <summary>
/// Configuration options for distributed caching.
/// </summary>
public sealed class CacheOptions
{
/// <summary>
/// Configuration section name in appsettings.json.
/// </summary>
public const string SectionName = "Cache";
/// <summary>
/// Signature cache expiration time.
/// If null, signatures will not expire automatically.
/// </summary>
public TimeSpan? SignatureCacheExpiration { get; set; }
}

View File

@@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using DigitalData.Core.Abstractions.Security.Extensions; using DigitalData.Core.Abstractions.Security.Extensions;
using EnvelopeGenerator.API.Middleware; using EnvelopeGenerator.API.Middleware;
using EnvelopeGenerator.API.Options;
using NLog.Web; using NLog.Web;
using NLog; using NLog;
using DigitalData.Auth.Claims; using DigitalData.Auth.Claims;
@@ -265,6 +266,20 @@ try
// Localizer // Localizer
builder.Services.AddCookieBasedLocalizer(); builder.Services.AddCookieBasedLocalizer();
// Cache options
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
// Distributed Cache - SQL Server
builder.Services.AddDistributedSqlServerCache(options =>
{
config.GetSection("Cache:SqlServer").Bind(options);
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
options.ConnectionString = connStr;
}
});
// Envelope generator serives // Envelope generator serives
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
builder.Services builder.Services

View File

@@ -174,6 +174,14 @@
"Receiver": [], "Receiver": [],
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ] "EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
}, },
"Cache": {
"SignatureCacheExpiration": null,
"SqlServer": {
"ConnectionString": null,
"SchemaName": "dbo",
"TableName": "TBDD_CACHE"
}
},
"MainPageTitle": null, "MainPageTitle": null,
"AnnotationParams": { "AnnotationParams": {
"Background": { "Background": {

View File

@@ -1,6 +1,7 @@
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Interfaces; using EnvelopeGenerator.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
@@ -8,7 +9,7 @@ namespace EnvelopeGenerator.Application.Common.Dto;
/// 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>
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
public class SignatureDto : ISignature public class DocReceiverElementDto : IDocReceiverElement
{ {
/// <summary> /// <summary>
/// Gets or sets the unique identifier of the element. /// Gets or sets the unique identifier of the element.
@@ -104,4 +105,24 @@ public class SignatureDto : ISignature
/// ///
/// </summary> /// </summary>
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp; public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
/// <summary>
///
/// </summary>
public string? FullName { get; set; }
/// <summary>
///
/// </summary>
public string? Position { get; set; }
/// <summary>
///
/// </summary>
public string? Place { get; set; }
/// <summary>
///
/// </summary>
public byte[]? Ink { get; set; }
} }

View File

@@ -31,5 +31,5 @@ public class DocumentDto
/// <summary> /// <summary>
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any. /// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
/// </summary> /// </summary>
public IEnumerable<SignatureDto>? Elements { get; set; } public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
} }

View File

@@ -23,7 +23,7 @@ public class MappingProfile : Profile
{ {
// Entity to DTO mappings // Entity to DTO mappings
CreateMap<Config, ConfigDto>(); CreateMap<Config, ConfigDto>();
CreateMap<Signature, SignatureDto>(); CreateMap<DocReceiverElement, DocReceiverElementDto>();
CreateMap<DocumentStatus, DocumentStatusDto>(); CreateMap<DocumentStatus, DocumentStatusDto>();
CreateMap<EmailTemplate, EmailTemplateDto>(); CreateMap<EmailTemplate, EmailTemplateDto>();
CreateMap<Envelope, EnvelopeDto>(); CreateMap<Envelope, EnvelopeDto>();
@@ -39,7 +39,7 @@ public class MappingProfile : Profile
// DTO to Entity mappings // DTO to Entity mappings
CreateMap<ConfigDto, Config>(); CreateMap<ConfigDto, Config>();
CreateMap<SignatureDto, Signature>(); CreateMap<DocReceiverElementDto, DocReceiverElement>();
CreateMap<DocumentStatusDto, DocumentStatus>(); CreateMap<DocumentStatusDto, DocumentStatus>();
CreateMap<EmailTemplateDto, EmailTemplate>(); CreateMap<EmailTemplateDto, EmailTemplate>();
CreateMap<EnvelopeDto, Envelope>(); CreateMap<EnvelopeDto, Envelope>();

View File

@@ -0,0 +1,11 @@
using System.Dynamic;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Represents PSPDFKit annotation data.
/// </summary>
/// <param name="Instant">Instant annotation data.</param>
/// <param name="Structured">Structured annotation data.</param>
[Obsolete("The PSPDFKit library is deprecated.")]
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);

View File

@@ -6,6 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
/// ///
/// </summary> /// </summary>
[Obsolete("Use IRepository")] [Obsolete("Use IRepository")]
public interface IDocumentReceiverElementRepository : ICRUDRepository<Signature, int> public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int>
{ {
} }

View File

@@ -8,6 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// ///
/// </summary> /// </summary>
[Obsolete("Use MediatR")] [Obsolete("Use MediatR")]
public interface IDocumentReceiverElementService : IBasicCRUDService<SignatureDto, Signature, int> public interface IDocumentReceiverElementService : IBasicCRUDService<DocReceiverElementDto, DocReceiverElement, int>
{ {
} }

View File

@@ -1,88 +1,37 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using MediatR; using MediatR;
using System.Dynamic;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned; namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
/// <summary> /// <summary>
/// /// Notification raised when a document is signed by a receiver.
/// </summary> /// </summary>
/// <param name="Instant"></param> [Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
/// <param name="Structured"></param> public record DocSignedNotification : INotification, ISendMailNotification
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
/// <summary>
///
/// </summary>
/// <param name="Original"></param>
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification, ISendMailNotification
{ {
/// <summary> /// <summary>
/// /// The envelope receiver information.
/// </summary> /// </summary>
public required EnvelopeReceiverDto EnvelopeReceiver { get; init; }
/// <summary>
/// The PSPDFKit annotation data.
/// </summary>
[Obsolete("The PSPDFKit library is deprecated.")]
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; } public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
/// <summary> /// <summary>
/// /// Gets the email template type.
/// </summary> /// </summary>
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned; public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
/// <summary> /// <summary>
/// /// Gets the email address of the receiver.
/// </summary> /// </summary>
public string EmailAddress => Receiver?.EmailAddress public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
?? throw new InvalidOperationException($"Receiver is null." + ?? throw new InvalidOperationException($"Receiver is null." +
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}"); $"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
}
/// <summary>
///
/// </summary>
public static class DocSignedNotificationExtensions
{
/// <summary>
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
/// </summary>
/// <param name="dto">The DTO to convert.</param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
/// <summary>
///
/// </summary>
/// <param name="dtoTask"></param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns></returns>
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
/// <summary>
///
/// </summary>
/// <param name="publisher"></param>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
{
try
{
await publisher.Publish(notification, cancel);
}
catch (Exception)
{
await publisher.Publish(new RemoveSignatureNotification()
{
EnvelopeId = notification.EnvelopeId,
ReceiverId = notification.ReceiverId
}, cancel);
throw;
}
}
} }

View File

@@ -1,5 +1,6 @@
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Application.Common.Dto;
using MediatR; using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers; namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("The PSPDFKit library is deprecated.")]
public class AnnotationHandler : INotificationHandler<DocSignedNotification> public class AnnotationHandler : INotificationHandler<DocSignedNotification>
{ {
/// <summary> /// <summary>

View File

@@ -1,4 +1,5 @@
using EnvelopeGenerator.Application.DocStatus.Commands; using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using MediatR; using MediatR;
using System.Text.Json; using System.Text.Json;
@@ -8,6 +9,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public class DocStatusHandler : INotificationHandler<DocSignedNotification> public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{ {
private const string BlankAnnotationJson = "{}"; private const string BlankAnnotationJson = "{}";
@@ -29,10 +31,11 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param> /// <param name="notification"></param>
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand() public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
{ {
EnvelopeId = notification.EnvelopeId, EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
ReceiverId = notification.ReceiverId, ReceiverId = notification.EnvelopeReceiver.ReceiverId,
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations) ? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson : BlankAnnotationJson

View File

@@ -29,13 +29,13 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
/// <returns></returns> /// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel) public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
{ {
if (notification.Receiver is null) if (notification.EnvelopeReceiver.Receiver is null)
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}"); throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
await _sender.Send(new CreateHistoryCommand() await _sender.Send(new CreateHistoryCommand()
{ {
EnvelopeId = notification.EnvelopeId, EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
UserReference = notification.Receiver.EmailAddress, UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress,
Status = EnvelopeStatus.DocumentSigned, Status = EnvelopeStatus.DocumentSigned,
}, cancel); }, cancel);
} }

View File

@@ -31,7 +31,7 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut) protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
{ {
emailOut.ReferenceString = notification.EmailAddress; emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.ReceiverId; emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
} }
/// <summary> /// <summary>
@@ -42,11 +42,11 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
{ {
var placeHolders = new Dictionary<string, string>() var placeHolders = new Dictionary<string, string>()
{ {
{ "[NAME_RECEIVER]", notification.Name ?? string.Empty }, { "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? string.Empty },
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty }, { "[DOCUMENT_TITLE]", notification.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
}; };
if (notification.Envelope.IsReadAndConfirm()) if (notification.EnvelopeReceiver.Envelope.IsReadAndConfirm())
{ {
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen"; placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty; placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;

View File

@@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using QRCoder; using QRCoder;
using System.Reflection; using System.Reflection;
using MediatR;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
namespace EnvelopeGenerator.Application; namespace EnvelopeGenerator.Application;
@@ -56,6 +59,22 @@ public static class DependencyInjection
services.AddMediatR(cfg => services.AddMediatR(cfg =>
{ {
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
// Register SignCommand pipeline behaviors in execution order
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
// 1. AnnotationBehavior - Saves annotations (executes second)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
// 2. DocStatusBehavior - Creates document status (executes third)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
// 3. HistoryBehavior - Records history (executes fourth)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
}); });
return services; return services;

View File

@@ -0,0 +1,41 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that saves annotations.
/// Executes first in the signing process.
/// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly IRepository<ElementAnnotation> _repo;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public AnnotationBehavior(IRepository<ElementAnnotation> repository)
{
_repo = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancellationToken);
return await next(cancellationToken);
}
}

View File

@@ -0,0 +1,50 @@
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using System.Text.Json;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that creates document status.
/// Executes second in the signing process.
/// </summary>
public class DocStatusBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private const string BlankAnnotationJson = "{}";
private readonly ISender _sender;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public DocStatusBehavior(ISender sender)
{
_sender = sender;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
await _sender.Send(new CreateDocStatusCommand()
{
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
ReceiverId = request.EnvelopeReceiver.ReceiverId,
Value = request.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson
}, cancellationToken);
return await next(cancellationToken);
}
}

View File

@@ -0,0 +1,54 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that resolves and validates EnvelopeReceiver.
/// Executes FIRST in the signing process - before all other behaviors.
/// If EnvelopeReceiver is not provided, it queries the database using EnvelopeReceiverQueryBase parameters.
/// </summary>
public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly IRepository<EnvelopeReceiver> _erRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="erRepo"></param>
/// <param name="mapper"></param>
public EnvelopeReceiverResolutionBehavior(IRepository<EnvelopeReceiver> erRepo, IMapper mapper)
{
_erRepo = erRepo;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
// If EnvelopeReceiver is not provided, query it from database
if (request.EnvelopeReceiver is null)
{
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
?? throw new NotFoundException("EnvelopeReceiver not found");
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
}
return await next(cancellationToken);
}
}

View File

@@ -0,0 +1,47 @@
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that records history.
/// Executes third in the signing process.
/// </summary>
public class HistoryBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly ISender _sender;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public HistoryBehavior(ISender sender)
{
_sender = sender;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
if (request.EnvelopeReceiver.Receiver is null)
throw new InvalidOperationException($"Receiver information is missing in the notification. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
await _sender.Send(new CreateHistoryCommand()
{
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
UserReference = request.EnvelopeReceiver.Receiver.EmailAddress,
Status = EnvelopeStatus.DocumentSigned,
}, cancellationToken);
return await next(cancellationToken);
}
}

View File

@@ -0,0 +1,51 @@
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using System.Text.Json;
<<<<<<< TODO: Unmerged change from project 'EnvelopeGenerator.Application (net8.0)', Before:
=======
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
>>>>>>> After
<<<<<<< TODO: Unmerged change from project 'EnvelopeGenerator.Application (net9.0)', Before:
=======
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
>>>>>>> After
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that creates document status.
/// Executes second in the signing process.
/// </summary>
public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly ISender _sender;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public SaveSignatureBehavior(ISender sender)
{
_sender = sender;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
return await next(cancellationToken);
}
}

View File

@@ -0,0 +1,122 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that sends signed mail notification.
/// Executes LAST in the signing process - only if all previous behaviors succeed.
/// </summary>
public class SendSignedMailBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly IRepository<EmailTemplate> _tempRepo;
private readonly IRepository<EmailOut> _emailOutRepo;
private readonly MailParams _mailParams;
private readonly DispatcherParams _dispatcherParams;
/// <summary>
///
/// </summary>
/// <param name="tempRepo"></param>
/// <param name="emailOutRepo"></param>
/// <param name="mailParamsOptions"></param>
/// <param name="dispatcherParamsOptions"></param>
public SendSignedMailBehavior(
IRepository<EmailTemplate> tempRepo,
IRepository<EmailOut> emailOutRepo,
IOptions<MailParams> mailParamsOptions,
IOptions<DispatcherParams> dispatcherParamsOptions)
{
_tempRepo = tempRepo;
_emailOutRepo = emailOutRepo;
_mailParams = mailParamsOptions.Value;
_dispatcherParams = dispatcherParamsOptions.Value;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
var placeHolders = CreatePlaceHolders(request);
var temp = await _tempRepo
.Where(x => x.Name == EmailTemplateType.DocumentSigned.ToString())
.SingleOrDefaultAsync(cancellationToken)
?? throw new InvalidOperationException($"Email template not found. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, _mailParams.Placeholders);
temp.Body = ReplacePlaceHolders(temp.Body, placeHolders, _mailParams.Placeholders);
var emailOut = new EmailOut
{
EmailAddress = request.EnvelopeReceiver.Receiver!.EmailAddress,
EmailBody = temp.Body,
EmailSubj = temp.Subject,
AddedWhen = DateTime.Now,
AddedWho = _dispatcherParams.AddedWho,
SendingProfile = _dispatcherParams.SendingProfile,
ReminderTypeId = _dispatcherParams.ReminderTypeId,
EmailAttmt1 = _dispatcherParams.EmailAttmt1,
WfId = (int)EnvelopeStatus.MessageConfirmationSent,
ReferenceString = request.EnvelopeReceiver.Receiver!.EmailAddress,
ReferenceId = request.EnvelopeReceiver.ReceiverId
};
await _emailOutRepo.CreateAsync(emailOut, cancellationToken);
return await next(cancellationToken);
}
private Dictionary<string, string> CreatePlaceHolders(SigningCommand request)
{
var placeHolders = new Dictionary<string, string>()
{
{ "[NAME_RECEIVER]", request.EnvelopeReceiver.Name ?? string.Empty },
{ "[DOCUMENT_TITLE]", request.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
};
if (request.EnvelopeReceiver.Envelope.IsReadAndConfirm())
{
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
}
else
{
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
placeHolders["[FINAL_STATUS]"] = "Signatur";
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
}
return placeHolders;
}
private static string ReplacePlaceHolders(string text, params Dictionary<string, string>[] placeHoldersList)
{
foreach (var placeHolders in placeHoldersList)
foreach (var ph in placeHolders)
text = text.Replace(ph.Key, ph.Value);
return text;
}
}

View File

@@ -0,0 +1,121 @@
using MediatR;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Query;
namespace EnvelopeGenerator.Application.DocReceiverElements.Commands;
/// <summary>
/// Represents a captured signature with metadata created by the receiver in the signature popup.
/// This model holds the signature image (as base64 data URL) along with signer information
/// used for rendering applied signatures on the PDF canvas.
/// </summary>
/// <remarks>
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
/// <br/>
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
/// </remarks>
public sealed record SignatureDto
{
/// <summary>
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
/// </summary>
public required int ElementId { get; init; }
/// <summary>
/// Base64-encoded data URL of the signature image.
/// <br/>
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
/// <br/>
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
/// <br/>
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
/// </summary>
public required string DataUrl { get; init; }
/// <summary>
/// Full name of the signer (first and last name).
/// <br/>
/// <b>Required:</b> Yes (validated in popup)
/// <br/>
/// <b>Example:</b> "Max Mustermann"
/// </summary>
public required string FullName { get; init; }
private readonly string? _position = null;
/// <summary>
/// Job title or position of the signer.
/// <br/>
/// <b>Required:</b> No (optional field)
/// <br/>
/// <b>Example:</b> "Geschäftsführer" or empty string
/// </summary>
public string? Position
{
get => _position;
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
}
/// <summary>
/// Location/place where the signature was created.
/// <br/>
/// <b>Required:</b> Yes (validated in popup)
/// <br/>
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
/// <br/>
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
/// </summary>
public required string Place { get; init; }
}
/// <summary>
/// Command to sign a document by a receiver.
/// </summary>
public record SigningCommand : EnvelopeReceiverQueryBase, IRequest
{
private EnvelopeReceiverDto? _envelopeReceiver;
internal void SetEnvelopeReceiver(EnvelopeReceiverDto envelopeReceiver)
{
_envelopeReceiver = envelopeReceiver;
}
/// <summary>
/// The envelope receiver information.
/// </summary>
public EnvelopeReceiverDto EnvelopeReceiver
{
get => _envelopeReceiver!;
init => _envelopeReceiver = value;
}
/// <summary>
/// The PSPDFKit annotation data.
/// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
/// <summary>
///
/// </summary>
public IEnumerable<SignatureDto>? Signatures { get; init; }
}
/// <summary>
/// Handles the sign command. All work is done by pipeline behaviors.
/// This handler is intentionally empty - behaviors handle all the processing.
/// </summary>
public class SignCommandHandler : IRequestHandler<SigningCommand>
{
/// <summary>
/// Executes the signing command. Pipeline behaviors handle all processing.
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task Handle(SigningCommand request, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,19 @@
using AutoMapper;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.DocReceiverElements;
/// <summary>
///
/// </summary>
public class MappingProfile : Profile
{
/// <summary>
///
/// </summary>
public MappingProfile()
{
CreateMap<SignatureDto, DocReceiverElement>();
}
}

View File

@@ -29,7 +29,7 @@ public record CreateEnvelopeReceiverCommand : CreateEnvelopeCommand, IRequest<Cr
/// <param name="X">X-Position</param> /// <param name="X">X-Position</param>
/// <param name="Y">Y-Position</param> /// <param name="Y">Y-Position</param>
/// <param name="Page">Seite, auf der sie sich befindet</param> /// <param name="Page">Seite, auf der sie sich befindet</param>
public record Signature([Required] double X, [Required] double Y, [Required] int Page); public record DocReceiverElementCreateDto([Required] double X, [Required] double Y, [Required] int Page);
/// <summary> /// <summary>
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen. /// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
@@ -41,7 +41,7 @@ public class ReceiverGetOrCreateCommand
/// Unterschriften auf Dokumenten. /// Unterschriften auf Dokumenten.
/// </summary> /// </summary>
[Required] [Required]
public List<Signature> Signatures { get; init; } = new(); public List<DocReceiverElementCreateDto> DocReceiverElements { get; init; } = new();
/// <summary> /// <summary>
/// Der Name, mit dem der Empfänger angesprochen werden soll. /// Der Name, mit dem der Empfänger angesprochen werden soll.

View File

@@ -1,9 +1,9 @@
using AutoMapper; using AutoMapper;
using DigitalData.Core.Application; using DigitalData.Core.Application;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Repositories; using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
using EnvelopeGenerator.Application.Common.Interfaces.Services; using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Services; namespace EnvelopeGenerator.Application.Services;
@@ -11,7 +11,7 @@ namespace EnvelopeGenerator.Application.Services;
/// ///
/// </summary> /// </summary>
[Obsolete("Use MediatR")] [Obsolete("Use MediatR")]
public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, SignatureDto, Signature, int>, IDocumentReceiverElementService public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, DocReceiverElementDto, DocReceiverElement, int>, IDocumentReceiverElementService
{ {
/// <summary> /// <summary>
/// ///

View File

@@ -164,7 +164,7 @@ Public Class frmFinalizePDF
Return Return
End If End If
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)() Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
Dim elements = sigRepo _ Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _ .Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
.Include(Function(sig) sig.Annotations) _ .Include(Function(sig) sig.Annotations) _

View File

@@ -46,7 +46,7 @@ Namespace Jobs.FinalizeDocument
Return pSourceBuffer Return pSourceBuffer
End If End If
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)() Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
Dim elements = sigRepo _ Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _ .Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
.Include(Function(sig) sig.Annotations) _ .Include(Function(sig) sig.Annotations) _
@@ -58,7 +58,7 @@ Namespace Jobs.FinalizeDocument
End Using End Using
End Function End Function
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of Signature)) As Byte() Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of DocReceiverElement)) As Byte()
' Add background ' Add background
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer) Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
'TODO: take the length from the largest y 'TODO: take the length from the largest y

View File

@@ -10,8 +10,8 @@ Public Class ElementModel
MyBase.New(pState) MyBase.New(pState)
End Sub End Sub
Private Function ToElement(pRow As DataRow) As Signature Private Function ToElement(pRow As DataRow) As DocReceiverElement
Return New Signature() With { Return New DocReceiverElement() With {
.Id = pRow.ItemEx("GUID", 0), .Id = pRow.ItemEx("GUID", 0),
.DocumentId = pRow.ItemEx("DOCUMENT_ID", 0), .DocumentId = pRow.ItemEx("DOCUMENT_ID", 0),
.ReceiverId = pRow.ItemEx("RECEIVER_ID", 0), .ReceiverId = pRow.ItemEx("RECEIVER_ID", 0),
@@ -24,7 +24,7 @@ Public Class ElementModel
} }
End Function End Function
Private Function ToElements(pTable As DataTable) As List(Of Signature) Private Function ToElements(pTable As DataTable) As List(Of DocReceiverElement)
Return pTable?.Rows.Cast(Of DataRow). Return pTable?.Rows.Cast(Of DataRow).
Select(AddressOf ToElement). Select(AddressOf ToElement).
ToList() ToList()
@@ -80,7 +80,7 @@ Public Class ElementModel
End Try End Try
End Function End Function
Public Function List(pDocumentId As Integer) As List(Of Signature) Public Function List(pDocumentId As Integer) As List(Of DocReceiverElement)
Try Try
Dim oSql = $"SELECT * FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE DOCUMENT_ID = {pDocumentId} ORDER BY PAGE ASC" Dim oSql = $"SELECT * FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE DOCUMENT_ID = {pDocumentId} ORDER BY PAGE ASC"
Dim oTable = Database.GetDatatable(oSql) Dim oTable = Database.GetDatatable(oSql)
@@ -93,7 +93,7 @@ Public Class ElementModel
End Try End Try
End Function End Function
Public Function List(pDocumentId As Integer, pReceiverId As Integer) As List(Of Signature) Public Function List(pDocumentId As Integer, pReceiverId As Integer) As List(Of DocReceiverElement)
Try Try
Dim oReceiverConstraint = "" Dim oReceiverConstraint = ""
If pReceiverId > 0 Then If pReceiverId > 0 Then
@@ -111,7 +111,7 @@ Public Class ElementModel
End Try End Try
End Function End Function
Public Function Insert(pElement As Signature) As Boolean Public Function Insert(pElement As DocReceiverElement) As Boolean
Try Try
Dim oSql = "INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] Dim oSql = "INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
([DOCUMENT_ID] ([DOCUMENT_ID]
@@ -161,7 +161,7 @@ Public Class ElementModel
End Try End Try
End Function End Function
Public Function Update(pElement As Signature) As Boolean Public Function Update(pElement As DocReceiverElement) As Boolean
Try Try
Dim oSql = "UPDATE [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] Dim oSql = "UPDATE [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
SET [POSITION_X] = @POSITION_X SET [POSITION_X] = @POSITION_X
@@ -185,7 +185,7 @@ Public Class ElementModel
End Try End Try
End Function End Function
Private Function GetElementId(pElement As Signature) As Integer Private Function GetElementId(pElement As DocReceiverElement) As Integer
Try Try
Return Database.GetScalarValue($"SELECT MAX(GUID) FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT Return Database.GetScalarValue($"SELECT MAX(GUID) FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT
WHERE DOCUMENT_ID = {pElement.DocumentId} AND RECEIVER_ID = {pElement.ReceiverId}") WHERE DOCUMENT_ID = {pElement.DocumentId} AND RECEIVER_ID = {pElement.ReceiverId}")
@@ -196,7 +196,7 @@ Public Class ElementModel
End Try End Try
End Function End Function
Public Function DeleteElement(pElement As Signature) As Boolean Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
Try Try
Dim oSql = $"DELETE FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT WHERE GUID = {pElement.Id}" Dim oSql = $"DELETE FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT WHERE GUID = {pElement.Id}"
Return Database.ExecuteNonQuery(oSql) Return Database.ExecuteNonQuery(oSql)

View File

@@ -11,9 +11,9 @@ 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 Signature : ISignature, IHasReceiver, IHasAddedWhen, IUpdateAuditable public class DocReceiverElement : IDocReceiverElement, IHasReceiver, IHasAddedWhen, IUpdateAuditable
{ {
public Signature() public DocReceiverElement()
{ {
// TODO: * Check the Form App and remove the default value // TODO: * Check the Form App and remove the default value
#if NETFRAMEWORK #if NETFRAMEWORK
@@ -126,5 +126,33 @@ namespace EnvelopeGenerator.Domain.Entities
[NotMapped] [NotMapped]
public double Left => Math.Round(X, 5); public double Left => Math.Round(X, 5);
#endif #endif
[Column("FULL_NAME", TypeName = "nvarchar(100)")]
public string
#if nullable
?
# endif
FullName { get; set; }
[Column("POSITION", TypeName = "nvarchar(100)")]
public string
#if nullable
?
#endif
Position { get; set; }
[Column("PLACE", TypeName = "nvarchar(100)")]
public string
#if nullable
?
#endif
Place { get; set; }
[Column("INK", TypeName = "varbinary(MAX)")]
public byte[]
#if nullable
?
#endif
Ink { get; set; }
} }
} }

View File

@@ -18,7 +18,7 @@ namespace EnvelopeGenerator.Domain.Entities
public Document() public Document()
{ {
#if NETFRAMEWORK #if NETFRAMEWORK
Elements = Enumerable.Empty<Signature>().ToList(); Elements = Enumerable.Empty<DocReceiverElement>().ToList();
#endif #endif
} }
@@ -60,7 +60,7 @@ namespace EnvelopeGenerator.Domain.Entities
FileNameOriginal { get; set; } FileNameOriginal { get; set; }
#endregion #endregion
public virtual List<Signature> public virtual List<DocReceiverElement>
#if nullable #if nullable
? ?
#endif #endif

View File

@@ -76,7 +76,7 @@ namespace EnvelopeGenerator.Domain.Entities
ChangedWho { get; set; } ChangedWho { get; set; }
[ForeignKey("ElementId")] [ForeignKey("ElementId")]
public virtual Signature public virtual DocReceiverElement
#if nullable #if nullable
? ?
#endif #endif

View File

@@ -1,6 +1,6 @@
namespace EnvelopeGenerator.Domain.Interfaces namespace EnvelopeGenerator.Domain.Interfaces
{ {
public interface ISignature public interface IDocReceiverElement
{ {
int Page { get; set; } int Page { get; set; }

View File

@@ -8,7 +8,7 @@ Public Class FieldEditorController
Inherits BaseController Inherits BaseController
Private ReadOnly Document As Document Private ReadOnly Document As Document
Public Property Elements As New List(Of Signature) Public Property Elements As New List(Of DocReceiverElement)
Public Class ElementInfo Public Class ElementInfo
Public ReceiverId As Integer Public ReceiverId As Integer
@@ -36,7 +36,7 @@ Public Class FieldEditorController
} }
End Function End Function
Private Function GetElementByPosition(pAnnotation As AnnotationStickyNote, pPage As Integer, pReceiverId As Integer) As Signature Private Function GetElementByPosition(pAnnotation As AnnotationStickyNote, pPage As Integer, pReceiverId As Integer) As DocReceiverElement
Dim oElement = Elements. Dim oElement = Elements.
Where(Function(e) Where(Function(e)
Return e.Left = CSng(Math.Round(pAnnotation.Left, 5)) And Return e.Left = CSng(Math.Round(pAnnotation.Left, 5)) And
@@ -47,12 +47,12 @@ Public Class FieldEditorController
Return oElement Return oElement
End Function End Function
Private Function GetElementByGuid(pGuid As Integer) As Signature Private Function GetElementByGuid(pGuid As Integer) As DocReceiverElement
Dim oElement = Elements.Where(Function(e) pGuid = e.Id).SingleOrDefault() Dim oElement = Elements.Where(Function(e) pGuid = e.Id).SingleOrDefault()
Return oElement Return oElement
End Function End Function
Public Function GetElement(pAnnotation As AnnotationStickyNote) As Signature Public Function GetElement(pAnnotation As AnnotationStickyNote) As DocReceiverElement
Dim oInfo = GetElementInfo(pAnnotation.Tag) Dim oInfo = GetElementInfo(pAnnotation.Tag)
If oInfo.Guid = -1 Then If oInfo.Guid = -1 Then
@@ -85,7 +85,7 @@ Public Class FieldEditorController
Else Else
Dim oInfo = GetElementInfo(pAnnotation.Tag) Dim oInfo = GetElementInfo(pAnnotation.Tag)
Elements.Add(New Signature() With { Elements.Add(New DocReceiverElement() With {
.ElementType = Constants.ElementType.Signature, .ElementType = Constants.ElementType.Signature,
.Height = oAnnotationHeight, .Height = oAnnotationHeight,
.Width = oAnnotationWidth, .Width = oAnnotationWidth,
@@ -98,7 +98,7 @@ Public Class FieldEditorController
End If End If
End Sub End Sub
Public Function ClearElements(pPage As Integer, pReceiverId As Integer) As IEnumerable(Of Signature) Public Function ClearElements(pPage As Integer, pReceiverId As Integer) As IEnumerable(Of DocReceiverElement)
Return Elements. Return Elements.
Where(Function(e) e.Page <> pPage And e.ReceiverId <> pReceiverId). Where(Function(e) e.Page <> pPage And e.ReceiverId <> pReceiverId).
ToList() ToList()
@@ -120,7 +120,7 @@ Public Class FieldEditorController
All(Function(pResult) pResult = True) All(Function(pResult) pResult = True)
End Function End Function
Public Function SaveElement(pElement As Signature) As Boolean Public Function SaveElement(pElement As DocReceiverElement) As Boolean
Try Try
If pElement.Id > 0 Then If pElement.Id > 0 Then
Return ElementModel.Update(pElement) Return ElementModel.Update(pElement)
@@ -134,11 +134,11 @@ Public Class FieldEditorController
End Try End Try
End Function End Function
Public Function DeleteElement(pElement As Signature) As Boolean Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
Try Try
' Element aus Datenbank löschen ' Element aus Datenbank löschen
If ElementModel.DeleteElement(pElement) Then If ElementModel.DeleteElement(pElement) Then
Dim oElement = New List(Of Signature)() From {pElement} Dim oElement = New List(Of DocReceiverElement)() From {pElement}
Elements = Elements.Except(oElement).ToList() Elements = Elements.Except(oElement).ToList()
Return True Return True
Else Else

View File

@@ -280,7 +280,7 @@ Partial Public Class frmFieldEditor
End If End If
End Sub End Sub
Private Sub LoadAnnotation(pElement As Signature, pReceiverId As Integer) Private Sub LoadAnnotation(pElement As DocReceiverElement, pReceiverId As Integer)
Dim oAnnotation As AnnotationStickyNote = Manager.AddStickyNoteAnnot(0, 0, 0, 0, "SIGNATUR") Dim oAnnotation As AnnotationStickyNote = Manager.AddStickyNoteAnnot(0, 0, 0, 0, "SIGNATUR")
Dim oPage = pElement.Page Dim oPage = pElement.Page
Dim oReceiver = Receivers.Where(Function(r) r.Id = pReceiverId).Single() Dim oReceiver = Receivers.Where(Function(r) r.Id = pReceiverId).Single()

View File

@@ -74,14 +74,14 @@ namespace EnvelopeGenerator.Infrastructure
services.AddSQLExecutor<Envelope>(); services.AddSQLExecutor<Envelope>();
services.AddSQLExecutor<Receiver>(); services.AddSQLExecutor<Receiver>();
services.AddSQLExecutor<Document>(); services.AddSQLExecutor<Document>();
services.AddSQLExecutor<Signature>(); services.AddSQLExecutor<DocReceiverElement>();
services.AddSQLExecutor<DocumentStatus>(); services.AddSQLExecutor<DocumentStatus>();
SetDapperTypeMap<Envelope>(); SetDapperTypeMap<Envelope>();
SetDapperTypeMap<User>(); SetDapperTypeMap<User>();
SetDapperTypeMap<Receiver>(); SetDapperTypeMap<Receiver>();
SetDapperTypeMap<Document>(); SetDapperTypeMap<Document>();
SetDapperTypeMap<Signature>(); SetDapperTypeMap<DocReceiverElement>();
SetDapperTypeMap<DocumentStatus>(); SetDapperTypeMap<DocumentStatus>();
services.AddScoped<IEnvelopeExecutor, EnvelopeExecutor>(); services.AddScoped<IEnvelopeExecutor, EnvelopeExecutor>();

View File

@@ -45,7 +45,7 @@ public abstract class EGDbContextBase : DbContext
public DbSet<Envelope> Envelopes { get; set; } public DbSet<Envelope> Envelopes { get; set; }
public DbSet<Signature> DocumentReceiverElements { get; set; } public DbSet<DocReceiverElement> DocumentReceiverElements { get; set; }
public DbSet<ElementAnnotation> DocumentReceiverElementAnnotations { get; set; } public DbSet<ElementAnnotation> DocumentReceiverElementAnnotations { get; set; }
@@ -154,7 +154,7 @@ public abstract class EGDbContextBase : DbContext
#endregion EnvelopeDocument #endregion EnvelopeDocument
#region DocumentReceiverElement #region DocumentReceiverElement
modelBuilder.Entity<Signature>() modelBuilder.Entity<DocReceiverElement>()
.HasOne(dre => dre.Document) .HasOne(dre => dre.Document)
.WithMany(ed => ed.Elements) .WithMany(ed => ed.Elements)
.HasForeignKey(dre => dre.DocumentId); .HasForeignKey(dre => dre.DocumentId);
@@ -196,7 +196,7 @@ public abstract class EGDbContextBase : DbContext
#endregion DocumentStatus #endregion DocumentStatus
#region Annotation #region Annotation
modelBuilder.Entity<Signature>() modelBuilder.Entity<DocReceiverElement>()
.HasMany(signature => signature.Annotations) .HasMany(signature => signature.Annotations)
.WithOne(annot => annot.Element) .WithOne(annot => annot.Element)
.HasForeignKey(annot => annot.ElementId); .HasForeignKey(annot => annot.ElementId);
@@ -217,7 +217,7 @@ public abstract class EGDbContextBase : DbContext
// TODO: call add trigger methods with attributes and reflection // TODO: call add trigger methods with attributes and reflection
AddTrigger<Config>(); AddTrigger<Config>();
AddTrigger<Signature>(); AddTrigger<DocReceiverElement>();
AddTrigger<DocumentStatus>(); AddTrigger<DocumentStatus>();
AddTrigger<EmailTemplate>(); AddTrigger<EmailTemplate>();
AddTrigger<Envelope>(); AddTrigger<Envelope>();

View File

@@ -6,7 +6,7 @@ using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Infrastructure.Repositories; namespace EnvelopeGenerator.Infrastructure.Repositories;
[Obsolete("Use IRepository")] [Obsolete("Use IRepository")]
public class DocumentReceiverElementRepository : CRUDRepository<Signature, int, EGDbContext>, IDocumentReceiverElementRepository public class DocumentReceiverElementRepository : CRUDRepository<DocReceiverElement, int, EGDbContext>, IDocumentReceiverElementRepository
{ {
public DocumentReceiverElementRepository(EGDbContext dbContext) : base(dbContext, dbContext.DocumentReceiverElements) public DocumentReceiverElementRepository(EGDbContext dbContext) : base(dbContext, dbContext.DocumentReceiverElements)
{ {

View File

@@ -103,7 +103,7 @@ namespace EnvelopeGenerator.PdfEditor
#endregion #endregion
public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthPx = 1.9500000000000002, double heightPx = 2.52) public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthPx = 1.9500000000000002, double heightPx = 2.52)
where TSignature : ISignature where TSignature : IDocReceiverElement
{ {
foreach (var signature in signatures) foreach (var signature in signatures)
Page(signature.Page, page => Page(signature.Page, page =>

View File

@@ -12,9 +12,11 @@
@inject IOptions<PdfViewerOptions> PdfViewerOptions @inject IOptions<PdfViewerOptions> PdfViewerOptions
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject SignatureService SignatureService @inject SignatureService SignatureService
@inject SignatureCacheService SignatureCacheService
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService @inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService @inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
@inject AppVersionService AppVersion @inject AppVersionService AppVersion
@inject ILogger<EnvelopeViewer> logger
@implements IAsyncDisposable @implements IAsyncDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -217,6 +219,20 @@
<div class="pdf-toolbar__divider"></div> <div class="pdf-toolbar__divider"></div>
@if (_totalSignatures > 0) { @if (_totalSignatures > 0) {
<div class="pdf-toolbar__section">
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change @(_capturedSignature is not null ? "pdf-toolbar__btn--signature-change-active" : "")"
disabled="@(_signedSignatures > 0)"
@onclick="HandleSignatureChangeClick"
title="@GetSignatureButtonTitle()">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
<span class="pdf-toolbar__btn-text">Unterschrift</span>
</button>
</div>
<div class="pdf-toolbar__divider"></div>
<div class="pdf-toolbar__section pdf-toolbar__signature-nav"> <div class="pdf-toolbar__section pdf-toolbar__signature-nav">
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-nav" <button class="pdf-toolbar__btn pdf-toolbar__btn--signature-nav"
@onclick="GoToPreviousSignature" @onclick="GoToPreviousSignature"
@@ -503,7 +519,7 @@ EnvelopeReceiverDto? _envelopeReceiver;
int _totalSignatures = 0; int _totalSignatures = 0;
int _signedSignatures = 0; int _signedSignatures = 0;
int _unsignedSignatures = 0; int _unsignedSignatures = 0;
int _currentSignatureIndex = 0; // Şu an hangi imzada (1-based) int _currentSignatureIndex = 0; // Current signature index (1-based)
// Signature state // Signature state
SignatureCaptureDto? _capturedSignature; SignatureCaptureDto? _capturedSignature;
@@ -563,10 +579,34 @@ const int MaxThumbnailWidth = 400;
await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures); await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures);
// Open signature popup on page load // Try to load cached signature first
_activeSignatureTab = SignatureTabDraw; try
_signaturePopupVisible = true; {
_popupValidationMessage = null; var cachedSignature = await SignatureCacheService.GetSignatureAsync(EnvelopeKey);
if (cachedSignature is not null)
{
_capturedSignature = cachedSignature;
_signerFullName = cachedSignature.FullName;
_signerPosition = cachedSignature.Position;
_signaturePlace = cachedSignature.Place;
_signaturePopupVisible = false;
logger.LogInformation("Cached signature loaded for envelope {EnvelopeKey}", EnvelopeKey);
}
else
{
_activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = true;
_popupValidationMessage = null;
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to load cached signature, showing popup");
_activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = true;
_popupValidationMessage = null;
}
} catch (Exception ex) { } catch (Exception ex) {
_errorMessage = $"Fehler: {ex.Message}"; _errorMessage = $"Fehler: {ex.Message}";
@@ -777,7 +817,7 @@ const int MaxThumbnailWidth = 400;
_totalSignatures = state.Total; _totalSignatures = state.Total;
_signedSignatures = state.Signed; _signedSignatures = state.Signed;
_unsignedSignatures = state.Unsigned; _unsignedSignatures = state.Unsigned;
_currentSignatureIndex = state.CurrentIndex; // Şu an hangi imzada _currentSignatureIndex = state.CurrentIndex; // Current signature
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} catch { } catch {
// Ignore errors during counter update // Ignore errors during counter update
@@ -799,15 +839,52 @@ const int MaxThumbnailWidth = 400;
record SignatureNavState(int Total, int Signed, int Unsigned, int CurrentIndex, bool CanGoPrev, bool CanGoNext); record SignatureNavState(int Total, int Signed, int Unsigned, int CurrentIndex, bool CanGoPrev, bool CanGoNext);
string GetSignatureButtonTitle()
{
if (_signedSignatures > 0)
return "Unterschrift ist gesperrt bitte Seite neu laden, um zu ändern";
return _capturedSignature is not null
? "Unterschrift ändern"
: "Unterschrift erstellen";
}
void HandleSignatureChangeClick()
{
// If any signature is applied, button is disabled - this won't be called
// But just in case, do nothing
if (_signedSignatures > 0)
return;
// No signatures applied - open popup normally
OpenSignaturePopup();
}
// Signature popup methods // Signature popup methods
void OpenSignaturePopup() { void OpenSignaturePopup() {
// Open popup with current signature (edit mode)
_activeSignatureTab = SignatureTabDraw; _activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = true; _signaturePopupVisible = true;
_popupValidationMessage = null; _popupValidationMessage = null;
// Load current signature info into form fields
if (_capturedSignature is not null)
{
_signerFullName = _capturedSignature.FullName;
_signerPosition = _capturedSignature.Position;
_signaturePlace = _capturedSignature.Place;
}
} }
async Task OnPopupShownAsync() { async Task OnPopupShownAsync() {
await InitializeActiveSignatureTabAsync(); await InitializeActiveSignatureTabAsync();
// If there's an existing signature and we're on draw tab, load it to canvas
if (_capturedSignature is not null && _activeSignatureTab == SignatureTabDraw)
{
await Task.Delay(100); // Wait for canvas to be ready
await JSRuntime.InvokeVoidAsync("receiverSignature.loadExistingSignature", DrawCanvasId, _capturedSignature.DataUrl);
}
} }
async Task SetSignatureTabAsync(string tab) { async Task SetSignatureTabAsync(string tab) {
@@ -881,6 +958,22 @@ const int MaxThumbnailWidth = 400;
}; };
_signaturePopupVisible = false; _signaturePopupVisible = false;
// Save to cache (fire-and-forget, ignore errors)
if (!string.IsNullOrWhiteSpace(EnvelopeKey))
{
_ = Task.Run(async () =>
{
try
{
await SignatureCacheService.SaveSignatureAsync(EnvelopeKey, _capturedSignature);
}
catch
{
// Ignore cache errors
}
});
}
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
Console.WriteLine($"Signature saved: {_signerFullName}, {_signaturePlace}"); Console.WriteLine($"Signature saved: {_signerFullName}, {_signaturePlace}");
} }

View File

@@ -16,12 +16,13 @@ builder.Services.Configure<ApiOptions>(opts =>
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts)); builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
builder.Services.Configure<PdfViewerOptions>(opts => builder.Services.Configure<PdfViewerOptions>(opts =>
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts)); builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.DocumentService>(); builder.Services.AddScoped<DocumentService>();
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AuthService>(); builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AnnotationService>(); builder.Services.AddScoped<AnnotationService>();
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService>(); builder.Services.AddScoped<EnvelopeReceiverService>();
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.SignatureService>(); builder.Services.AddScoped<SignatureService>();
builder.Services.AddSingleton<EnvelopeGenerator.ReceiverUI.Services.AppVersionService>(); builder.Services.AddScoped<SignatureCacheService>();
builder.Services.AddSingleton<AppVersionService>();
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer(); builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer(); builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();

View File

@@ -0,0 +1,67 @@
using System.Net.Http.Json;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.ReceiverUI.Options;
using EnvelopeGenerator.ReceiverUI.Models;
namespace EnvelopeGenerator.ReceiverUI.Services;
/// <summary>
/// Client service for managing cached signatures via API.
/// </summary>
public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOptions)
{
private readonly ApiOptions _api = apiOptions.Value;
public async Task SaveSignatureAsync(
string envelopeKey,
SignatureCaptureDto signature,
CancellationToken cancel = default)
{
var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
signature,
cancel);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(cancel);
throw new HttpRequestException($"Failed to cache signature: {response.StatusCode} - {error}");
}
}
public async Task<SignatureCaptureDto?> GetSignatureAsync(
string envelopeKey,
CancellationToken cancel = default)
{
var response = await http.GetAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
cancel);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
return null;
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(cancel);
throw new HttpRequestException($"Failed to retrieve signature: {response.StatusCode} - {error}");
}
return await response.Content.ReadFromJsonAsync<SignatureCaptureDto>(cancellationToken: cancel);
}
public async Task DeleteSignatureAsync(
string envelopeKey,
CancellationToken cancel = default)
{
var response = await http.DeleteAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
cancel);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(cancel);
throw new HttpRequestException($"Failed to delete signature: {response.StatusCode} - {error}");
}
}
}

View File

@@ -484,6 +484,68 @@ body.resizing {
color: white; color: white;
} }
/* Success Button Styles (Signature Created) */
.pdf-toolbar__btn--success {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
border-color: rgba(16, 185, 129, 0.3);
color: #059669;
}
.pdf-toolbar__btn--success:hover:not(:disabled) {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border-color: transparent;
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.pdf-toolbar__btn--success svg {
transition: color 0.2s ease;
}
.pdf-toolbar__btn--success:hover:not(:disabled) svg {
color: white;
}
/* Signature Change Button */
.pdf-toolbar__btn--signature-change {
display: flex;
align-items: center;
gap: 0.375rem;
min-width: auto;
padding: 0.5rem 0.75rem;
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
border: 1px solid rgba(126, 34, 206, 0.2);
color: #7e22ce;
}
.pdf-toolbar__btn--signature-change:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
border-color: rgba(126, 34, 206, 0.4);
}
.pdf-toolbar__btn--signature-change-active {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(5, 150, 105, 0.08) 100%);
border-color: rgba(16, 185, 129, 0.25);
color: #059669;
}
.pdf-toolbar__btn--signature-change-active:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%);
border-color: rgba(16, 185, 129, 0.35);
}
.pdf-toolbar__btn--signature-change:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pdf-toolbar__btn-text {
font-size: 0.813rem;
font-weight: 600;
white-space: nowrap;
}
.pdf-frame { .pdf-frame {
background: white; background: white;
border-radius: 16px; border-radius: 16px;

View File

@@ -489,7 +489,7 @@ window.pdfViewer = {
* @returns {object} { total, signed, unsigned, currentIndex, canGoPrev, canGoNext } * @returns {object} { total, signed, unsigned, currentIndex, canGoPrev, canGoNext }
*/ */
getSignatureNavState() { getSignatureNavState() {
// Global imza listesi yoksa bo? state d<>n // Return empty state if global signature list is empty
if (!this._allSignatures || this._allSignatures.length === 0) { if (!this._allSignatures || this._allSignatures.length === 0) {
return { return {
total: 0, total: 0,
@@ -501,25 +501,26 @@ window.pdfViewer = {
}; };
} }
// T<>M sayfalardaki imzalar? say (database'den gelen global liste)
const total = this._allSignatures.length; // Global: Toplam imza say?s?
const signed = this.appliedSignatures.length; // ?mzalananlar
const unsigned = total - signed; // Hesaplanan: ?mzalanmayanlar
// Mevcut görüntülenen imzanın sıra numarasını bul // Count signatures across ALL pages (from database global list)
const total = this._allSignatures.length; // Global: Total signature count
const signed = this.appliedSignatures.length; // Signed signatures
const unsigned = total - signed; // Calculated: Unsigned signatures
// Find the current viewed signature index
let currentIndex = 0; let currentIndex = 0;
if (this._lastViewedSignatureId) { if (this._lastViewedSignatureId) {
const index = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId); const index = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
currentIndex = index !== -1 ? index + 1 : 0; // 1-based index (kullanıcıya göstermek için) currentIndex = index !== -1 ? index + 1 : 0; // 1-based index (for user display)
} }
return { return {
total: total, total: total,
signed: signed, signed: signed,
unsigned: unsigned, unsigned: unsigned,
currentIndex: currentIndex, // Şu an hangi imzada (1-5 arası) currentIndex: currentIndex, // Current signature index (1-5 range)
canGoPrev: total > 0, // Her zaman aktif (e?er imza varsa) canGoPrev: total > 0, // Always active (if signatures exist)
canGoNext: total > 0 // Her zaman aktif (e?er imza varsa) canGoNext: total > 0 // Always active (if signatures exist)
}; };
}, },
@@ -529,63 +530,64 @@ window.pdfViewer = {
* Cross-page navigation: searches ALL pages for next unsigned signature. * Cross-page navigation: searches ALL pages for next unsigned signature.
*/ */
async goToNextSignature(dotNetRef) { async goToNextSignature(dotNetRef) {
// Global imza listesi yoksa <20>?k // Exit if no global signature list
if (!this._allSignatures || this._allSignatures.length === 0) { if (!this._allSignatures || this._allSignatures.length === 0) {
return false; return false;
} }
// Mevcut g<>r<EFBFBD>nt<6E>lenen imzan?n index'ini bul // Find current displayed signature's index
let currentIndex = -1; let currentIndex = -1;
if (this._lastViewedSignatureId) { if (this._lastViewedSignatureId) {
currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId); currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
} }
// Bir sonraki imzay? al (imzalanm?? olup olmad???na bakmadan) // Get next signature (regardless of signed status)
let nextIndex = currentIndex + 1; let nextIndex = currentIndex + 1;
// Sonsuz d<>ng<6E>: Son imzadaysa ilk imzaya d<>n // Infinite loop: If at last signature, return to first
if (nextIndex >= this._allSignatures.length) { if (nextIndex >= this._allSignatures.length) {
nextIndex = 0; // ?lk imzaya d<>n nextIndex = 0; // Return to first signature
} }
const nextSignature = this._allSignatures[nextIndex]; const nextSignature = this._allSignatures[nextIndex];
// Farkl? sayfadaysa sayfa de?i?tir // Change page if signature is on different page
if (nextSignature.page !== this.pageNum) { if (nextSignature.page !== this.pageNum) {
// Sayfa de?i?tir // Change page
this.pageNum = nextSignature.page; this.pageNum = nextSignature.page;
this.queueRenderPage(this.pageNum); this.queueRenderPage(this.pageNum);
// Render tamamlanana kadar bekle // Wait until render completes
let waitCount = 0; let waitCount = 0;
while (this.pageRendering && waitCount < 20) { while (this.pageRendering && waitCount < 20) {
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
waitCount++; waitCount++;
} }
// Blazor'a haber ver - signature butonlar?n? yeniden ?iz // Notify Blazor - re-render signature buttons
if (dotNetRef) { if (dotNetRef) {
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum); await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
} }
// Butonlar?n DOM'a eklenmesini bekle // Wait for buttons to be added to DOM
await new Promise(resolve => setTimeout(resolve, 150)); await new Promise(resolve => setTimeout(resolve, 150));
} }
// Son g<>r<EFBFBD>nt<6E>lenen imzay? kaydet
// Save last viewed signature
this._lastViewedSignatureId = nextSignature.id; this._lastViewedSignatureId = nextSignature.id;
// ?mza imzalanm?? m? kontrol et // Check if signature is signed
const isApplied = this.appliedSignatures.some(s => s.id === nextSignature.id); const isApplied = this.appliedSignatures.some(s => s.id === nextSignature.id);
if (isApplied) { if (isApplied) {
// ?mzalanm?? - overlay container'? bul ve scroll yap // Signed - find overlay container and scroll
const container = document.querySelector(`.applied-signature[data-signature-id="${nextSignature.id}"]`); const container = document.querySelector(`.applied-signature[data-signature-id="${nextSignature.id}"]`);
if (container) { if (container) {
this.scrollToElement(container); this.scrollToElement(container);
} }
} else { } else {
// ?mzalanmam?? - butonu bul ve scroll yap // Unsigned - find button and scroll
const button = this.signatureButtons.find(btn => const button = this.signatureButtons.find(btn =>
parseInt(btn.getAttribute('data-signature-id')) === nextSignature.id parseInt(btn.getAttribute('data-signature-id')) === nextSignature.id
); );
@@ -594,7 +596,7 @@ window.pdfViewer = {
} }
} }
// Counter'? g<>ncelle (Blazor'a bildir) // Update counter (notify Blazor)
if (dotNetRef) { if (dotNetRef) {
dotNetRef.invokeMethodAsync('OnSignatureNavChanged'); dotNetRef.invokeMethodAsync('OnSignatureNavChanged');
} }
@@ -611,58 +613,58 @@ window.pdfViewer = {
return false; return false;
} }
// Mevcut g<>r<EFBFBD>nt<6E>lenen imzan?n index'ini bul // Find current displayed signature's index
let currentIndex = this._allSignatures.length; // Varsay?lan: son imzadan sonra let currentIndex = this._allSignatures.length; // Default: after last signature
if (this._lastViewedSignatureId) { if (this._lastViewedSignatureId) {
currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId); currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
} }
// Bir <20>nceki imzay? al // Get previous signature
let prevIndex = currentIndex - 1; let prevIndex = currentIndex - 1;
// Sonsuz d<>ng<6E>: ?lk imzadaysa son imzaya git // Infinite loop: If at first signature, go to last
if (prevIndex < 0) { if (prevIndex < 0) {
prevIndex = this._allSignatures.length - 1; // Son imzaya git prevIndex = this._allSignatures.length - 1; // Go to last signature
} }
const prevSignature = this._allSignatures[prevIndex]; const prevSignature = this._allSignatures[prevIndex];
// Change page if needed // Change page if needed
if (prevSignature.page !== this.pageNum) { if (prevSignature.page !== this.pageNum) {
// Sayfa de?i?tir // Change page
this.pageNum = prevSignature.page; this.pageNum = prevSignature.page;
this.queueRenderPage(this.pageNum); this.queueRenderPage(this.pageNum);
// Render tamamlanana kadar bekle // Wait until render completes
let waitCount = 0; let waitCount = 0;
while (this.pageRendering && waitCount < 20) { while (this.pageRendering && waitCount < 20) {
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
waitCount++; waitCount++;
} }
// Blazor'a haber ver - signature butonlar?n? yeniden <20>iz // Notify Blazor - re-render signature buttons
if (dotNetRef) { if (dotNetRef) {
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum); await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
} }
// DOM g<>ncellenmesini bekle // Wait for DOM update
await new Promise(resolve => setTimeout(resolve, 150)); await new Promise(resolve => setTimeout(resolve, 150));
} }
// Son g<>r<EFBFBD>nt<6E>lenen imzay? kaydet // Save last viewed signature
this._lastViewedSignatureId = prevSignature.id; this._lastViewedSignatureId = prevSignature.id;
// ?mza imzalanm?? m? kontrol et // Check if signature is signed
const isApplied = this.appliedSignatures.some(s => s.id === prevSignature.id); const isApplied = this.appliedSignatures.some(s => s.id === prevSignature.id);
if (isApplied) { if (isApplied) {
// ?mzalanm?? - overlay container'? bul ve scroll yap // Signed - find overlay container and scroll
const container = document.querySelector(`.applied-signature[data-signature-id="${prevSignature.id}"]`); const container = document.querySelector(`.applied-signature[data-signature-id="${prevSignature.id}"]`);
if (container) { if (container) {
this.scrollToElement(container); this.scrollToElement(container);
} }
} else { } else {
// ?mzalanmam?? - butonu bul ve scroll yap // Unsigned - find button and scroll
const button = this.signatureButtons.find(btn => const button = this.signatureButtons.find(btn =>
parseInt(btn.getAttribute('data-signature-id')) === prevSignature.id parseInt(btn.getAttribute('data-signature-id')) === prevSignature.id
); );
@@ -1070,8 +1072,8 @@ window.pdfViewer = {
// Text information container // Text information container
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'signature-info-text'; // ✅ Class ekle (querySelector için) infoContainer.className = 'signature-info-text'; // ✅ Add class (for querySelector)
infoContainer.setAttribute('data-base-font-size', '9'); // ✅ Base font size sakla infoContainer.setAttribute('data-base-font-size', '9'); // ✅ Store base font size
infoContainer.style.fontSize = '9px'; infoContainer.style.fontSize = '9px';
infoContainer.style.lineHeight = '1.4'; infoContainer.style.lineHeight = '1.4';
infoContainer.style.color = '#495057'; infoContainer.style.color = '#495057';

View File

@@ -316,6 +316,23 @@ window.receiverSignature = (() => {
function getTypedDataUrl(id) { const c = document.getElementById(id); const s = typedSignatures.get(id); return (c && s && s.hasSignature) ? c.toDataURL('image/png') : null; } function getTypedDataUrl(id) { const c = document.getElementById(id); const s = typedSignatures.get(id); return (c && s && s.hasSignature) ? c.toDataURL('image/png') : null; }
function getImageDataUrl(id) { const c = document.getElementById(id); const s = imageSignatures.get(id); return (c && s && s.hasSignature) ? c.toDataURL('image/png') : null; } function getImageDataUrl(id) { const c = document.getElementById(id); const s = imageSignatures.get(id); return (c && s && s.hasSignature) ? c.toDataURL('image/png') : null; }
function loadExistingSignature(canvasId, dataUrl) {
const canvas = document.getElementById(canvasId);
const state = pads.get(canvasId);
if (!canvas || !state || !dataUrl) return;
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
_clear(canvas);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
state.hasSignature = true;
};
img.src = dataUrl;
}
// ?? Public API ?????????????????????????????????????????????????????????? // ?? Public API ??????????????????????????????????????????????????????????
return { return {
startTyped: startTyped, startTyped: startTyped,
@@ -329,6 +346,8 @@ window.receiverSignature = (() => {
renderTypedSignature: renderTypedSignature, renderTypedSignature: renderTypedSignature,
getDataUrl: getDataUrl, getDataUrl: getDataUrl,
getTypedDataUrl: getTypedDataUrl, getTypedDataUrl: getTypedDataUrl,
getImageDataUrl: getImageDataUrl getImageDataUrl: getImageDataUrl,
loadExistingSignature: loadExistingSignature
}; };
})(); })();

View File

@@ -2,6 +2,7 @@
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Infrastructure; using DigitalData.Core.Infrastructure;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned; using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers; using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
@@ -53,7 +54,7 @@ public class DocSignedNotificationTests : TestBase
var annots = Services.GetRequiredService<PsPdfKitAnnotation>(); var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots); var docSignedNtf = new DocSignedNotification { EnvelopeReceiver = envRcvDto, PsPdfKitAnnotation = annots };
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>(); var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();

View File

@@ -1,8 +1,10 @@
using DigitalData.Core.Abstraction.Application.DTO; using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.Exceptions; using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services; using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned; using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Histories.Queries; using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
@@ -45,6 +47,7 @@ public class AnnotationController : ControllerBase
[Authorize(Roles = Role.ReceiverFull)] [Authorize(Roles = Role.ReceiverFull)]
[HttpPost] [HttpPost]
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default) public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{ {
// get claims // get claims
@@ -69,12 +72,24 @@ public class AnnotationController : ControllerBase
else if (er.Envelope.IsReadAndSign() && await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel)) else if (er.Envelope.IsReadAndSign() && await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
return Problem(statusCode: 423); return Problem(statusCode: 423);
var docSignedNotification = await _mediator var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
.ReadEnvelopeReceiverAsync(uuid, signature, cancel) var docSignedNotification = envelopeReceiverDto is not null
.ToDocSignedNotification(psPdfKitAnnotation) ? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
?? throw new NotFoundException("Envelope receiver is not found."); : throw new NotFoundException("Envelope receiver is not found.");
await _mediator.PublishSafely(docSignedNotification, cancel); try
{
await _mediator.Publish(docSignedNotification, cancel);
}
catch (Exception)
{
await _mediator.Publish(new RemoveSignatureNotification()
{
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
}, cancel);
throw;
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

View File

@@ -5,7 +5,7 @@ using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Web.Controllers.Test; namespace EnvelopeGenerator.Web.Controllers.Test;
[Obsolete("Use MediatR")] [Obsolete("Use MediatR")]
public class TestDocumentReceiverElementController : TestControllerBase<IDocumentReceiverElementService, SignatureDto, Signature, int> public class TestDocumentReceiverElementController : TestControllerBase<IDocumentReceiverElementService, DocReceiverElementDto, DocReceiverElement, int>
{ {
public TestDocumentReceiverElementController(ILogger<TestDocumentReceiverElementController> logger, IDocumentReceiverElementService service) : base(logger, service) public TestDocumentReceiverElementController(ILogger<TestDocumentReceiverElementController> logger, IDocumentReceiverElementService service) : base(logger, service)
{ {