Compare commits
43 Commits
120485ee8d
...
a0ed3e2fe4
| Author | SHA1 | Date | |
|---|---|---|---|
| a0ed3e2fe4 | |||
| f5505190e9 | |||
| 1bfdbac8ff | |||
| 4a29511491 | |||
| 1089304bf1 | |||
| 9b606a0d3b | |||
| cb6dea319b | |||
| d59aa6157d | |||
| 1569647b60 | |||
| 3bb2a013ab | |||
| 215b755f92 | |||
| 3a94733047 | |||
| 7793d3cbb9 | |||
| 9174065365 | |||
| 19824afc1c | |||
| c7fe3f0b9c | |||
| a98024063a | |||
| e0cab3f965 | |||
| e5347b063d | |||
| ecd695ad37 | |||
| 5a8809ffc1 | |||
| b832637a6a | |||
| fceb266630 | |||
| 87bdef9d5e | |||
| 2fb32fb982 | |||
| 63d050244c | |||
| 126a4acb12 | |||
| 082cb322ef | |||
| cd077aa1bd | |||
| 49ac35153e | |||
| 91a563d995 | |||
| 308cdd03f2 | |||
| f35068e368 | |||
| e490805025 | |||
| 08550f87e6 | |||
| a22ec7a7d3 | |||
| f4681f85e7 | |||
| 0ed4a44df0 | |||
| b8926f25c4 | |||
| 5b220932d3 | |||
| 50c02314ef | |||
| 223bb88f54 | |||
| 38f4da00da |
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
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.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
@@ -13,7 +13,6 @@ using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
@@ -62,8 +61,8 @@ public class AnnotationController : ControllerBase
|
||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||
{
|
||||
var signature = User.GetReceiverSignatureOfReceiver();
|
||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||
var signature = User.ReceiverSignature();
|
||||
var uuid = User.EnvelopeUuid();
|
||||
|
||||
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))
|
||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||
|
||||
var docSignedNotification = await _mediator
|
||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
||||
var docSignedNotification = envelopeReceiverDto is not null
|
||||
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
||||
: 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);
|
||||
|
||||
return Ok();
|
||||
@@ -95,9 +106,9 @@ public class AnnotationController : ControllerBase
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||
{
|
||||
var signature = User.GetReceiverSignatureOfReceiver();
|
||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||
var mail = User.GetReceiverMailOfReceiver();
|
||||
var signature = User.ReceiverSignature();
|
||||
var uuid = User.EnvelopeUuid();
|
||||
var mail = User.ReceiverMail();
|
||||
|
||||
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||
|
||||
|
||||
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal file
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal 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);
|
||||
@@ -51,7 +51,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
|
||||
if (query is not null)
|
||||
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);
|
||||
return receiverDoc.ByteData is byte[] receiverDocByte
|
||||
? File(receiverDocByte, "application/octet-stream")
|
||||
@@ -71,7 +71,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
|
||||
[HttpGet("{envelopeKey}")]
|
||||
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);
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ public class EnvelopeReceiverController : ControllerBase
|
||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||
|
||||
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);
|
||||
conn.Open();
|
||||
|
||||
@@ -41,14 +41,14 @@ public class ReadOnlyController : ControllerBase
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||
{
|
||||
var authReceiverMail = User.GetReceiverMailOfReceiver();
|
||||
var authReceiverMail = User.ReceiverMail();
|
||||
if (authReceiverMail is null)
|
||||
{
|
||||
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var envelopeId = User.GetEnvelopeIdOfReceiver();
|
||||
var envelopeId = User.EnvelopeId();
|
||||
|
||||
createDto.AddedWho = authReceiverMail;
|
||||
createDto.EnvelopeId = envelopeId;
|
||||
|
||||
@@ -36,15 +36,15 @@ public class SignatureController : ControllerBase
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[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);
|
||||
|
||||
if (doc.Elements is not IEnumerable<SignatureDto> docSignatures)
|
||||
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
|
||||
return NotFound("Document is empty.");
|
||||
|
||||
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
|
||||
|
||||
@@ -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)}";
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<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.bouncy-castle-adapter" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Linq;
|
||||
using DigitalData.Auth.Claims;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using System.Security.Claims;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
||||
namespace EnvelopeGenerator.API.Extensions;
|
||||
|
||||
@@ -11,12 +9,14 @@ namespace EnvelopeGenerator.API.Extensions;
|
||||
/// </summary>
|
||||
public static class ReceiverClaimExtensions
|
||||
{
|
||||
private static readonly string[] EnvelopeIdClaimTypes = [EnvelopeClaimTypes.Id, "envelope_id", "EnvelopeId"];
|
||||
private static readonly string[] ReceiverIdClaimTypes = ["receiver_id", "ReceiverId"];
|
||||
private static readonly string[] EnvelopeUuidClaimTypes = [ClaimTypes.NameIdentifier, "envelope_uuid", "EnvelopeUuid"];
|
||||
private static readonly string[] ReceiverSignatureClaimTypes = [ClaimTypes.Hash, "receiver_sig", "ReceiverSignature"];
|
||||
|
||||
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, string claimType)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="claimType"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private static string GetRequiredClaimValue(this ClaimsPrincipal user, string claimType)
|
||||
{
|
||||
var value = user.FindFirstValue(claimType);
|
||||
if (value is not null)
|
||||
@@ -32,7 +32,7 @@ public static class ReceiverClaimExtensions
|
||||
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())
|
||||
{
|
||||
@@ -52,89 +52,45 @@ public static class ReceiverClaimExtensions
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope UUID from the claims.
|
||||
/// </summary>
|
||||
public static string GetEnvelopeUuidOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeUuidClaimTypes);
|
||||
public static string EnvelopeUuid(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver signature from the claims.
|
||||
/// </summary>
|
||||
public static string GetReceiverSignatureOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ReceiverSignatureClaimTypes);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver display name from the claims.
|
||||
/// </summary>
|
||||
public static string GetReceiverNameOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Name);
|
||||
public static string ReceiverSignature(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver email address from the claims.
|
||||
/// </summary>
|
||||
public static string GetReceiverMailOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Email);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope title from the claims.
|
||||
/// </summary>
|
||||
public static string GetEnvelopeTitleOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Title);
|
||||
public static string ReceiverMail(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope identifier from the claims.
|
||||
/// </summary>
|
||||
public static int GetEnvelopeIdOfReceiver(this ClaimsPrincipal user)
|
||||
public static int EnvelopeId(this ClaimsPrincipal user)
|
||||
{
|
||||
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeIdClaimTypes);
|
||||
if (!int.TryParse(envIdStr, out var envId))
|
||||
{
|
||||
throw new InvalidOperationException($"Claim '{"envelope_id"}' is not a valid integer.");
|
||||
}
|
||||
|
||||
var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId);
|
||||
if (int.TryParse(envIdStr, out var envId))
|
||||
return envId;
|
||||
else
|
||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets the authenticated receiver identifier from the claims.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static int GetReceiverIdOfReceiver(this ClaimsPrincipal user)
|
||||
public static int ReceiverId(this ClaimsPrincipal user)
|
||||
{
|
||||
var rcvIdStr = user.GetRequiredClaimOfReceiver(ReceiverIdClaimTypes);
|
||||
if (!int.TryParse(rcvIdStr, out var rcvId))
|
||||
{
|
||||
throw new InvalidOperationException($"Claim '{"receiver_id"}' is not a valid integer.");
|
||||
}
|
||||
|
||||
var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
|
||||
if (int.TryParse(rcvIdStr, out var rcvId))
|
||||
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);
|
||||
else
|
||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
|
||||
}
|
||||
}
|
||||
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal file
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal 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; }
|
||||
}
|
||||
@@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||
using EnvelopeGenerator.API.Middleware;
|
||||
using EnvelopeGenerator.API.Options;
|
||||
using NLog.Web;
|
||||
using NLog;
|
||||
using DigitalData.Auth.Claims;
|
||||
@@ -265,6 +266,20 @@ try
|
||||
// Localizer
|
||||
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
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
builder.Services
|
||||
|
||||
@@ -174,6 +174,14 @@
|
||||
"Receiver": [],
|
||||
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
||||
},
|
||||
"Cache": {
|
||||
"SignatureCacheExpiration": null,
|
||||
"SqlServer": {
|
||||
"ConnectionString": null,
|
||||
"SchemaName": "dbo",
|
||||
"TableName": "TBDD_CACHE"
|
||||
}
|
||||
},
|
||||
"MainPageTitle": null,
|
||||
"AnnotationParams": {
|
||||
"Background": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class SignatureDto : ISignature
|
||||
public class DocReceiverElementDto : IDocReceiverElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the element.
|
||||
@@ -104,4 +105,24 @@ public class SignatureDto : ISignature
|
||||
///
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
@@ -31,5 +31,5 @@ public class DocumentDto
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||
/// </summary>
|
||||
public IEnumerable<SignatureDto>? Elements { get; set; }
|
||||
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class MappingProfile : Profile
|
||||
{
|
||||
// Entity to DTO mappings
|
||||
CreateMap<Config, ConfigDto>();
|
||||
CreateMap<Signature, SignatureDto>();
|
||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>();
|
||||
@@ -39,7 +39,7 @@ public class MappingProfile : Profile
|
||||
|
||||
// DTO to Entity mappings
|
||||
CreateMap<ConfigDto, Config>();
|
||||
CreateMap<SignatureDto, Signature>();
|
||||
CreateMap<DocReceiverElementDto, DocReceiverElement>();
|
||||
CreateMap<DocumentStatusDto, DocumentStatus>();
|
||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||
CreateMap<EnvelopeDto, Envelope>();
|
||||
|
||||
@@ -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);
|
||||
@@ -6,6 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Use IRepository")]
|
||||
public interface IDocumentReceiverElementRepository : ICRUDRepository<Signature, int>
|
||||
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int>
|
||||
{
|
||||
}
|
||||
@@ -8,6 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Use MediatR")]
|
||||
public interface IDocumentReceiverElementService : IBasicCRUDService<SignatureDto, Signature, int>
|
||||
public interface IDocumentReceiverElementService : IBasicCRUDService<DocReceiverElementDto, DocReceiverElement, int>
|
||||
{
|
||||
}
|
||||
@@ -1,88 +1,37 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Notification raised when a document is signed by a receiver.
|
||||
/// </summary>
|
||||
/// <param name="Instant"></param>
|
||||
/// <param name="Structured"></param>
|
||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Original"></param>
|
||||
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification, ISendMailNotification
|
||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||
public record DocSignedNotification : INotification, ISendMailNotification
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// The envelope receiver information.
|
||||
/// </summary>
|
||||
public required EnvelopeReceiverDto EnvelopeReceiver { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The PSPDFKit annotation data.
|
||||
/// </summary>
|
||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets the email template type.
|
||||
/// </summary>
|
||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets the email address of the receiver.
|
||||
/// </summary>
|
||||
public string EmailAddress => Receiver?.EmailAddress
|
||||
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||
?? throw new InvalidOperationException($"Receiver is null." +
|
||||
$"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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
@@ -8,6 +9,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private const string BlankAnnotationJson = "{}";
|
||||
@@ -29,10 +31,11 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
||||
{
|
||||
EnvelopeId = notification.EnvelopeId,
|
||||
ReceiverId = notification.ReceiverId,
|
||||
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||
ReceiverId = notification.EnvelopeReceiver.ReceiverId,
|
||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||
: BlankAnnotationJson
|
||||
|
||||
@@ -29,13 +29,13 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <returns></returns>
|
||||
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)}");
|
||||
|
||||
await _sender.Send(new CreateHistoryCommand()
|
||||
{
|
||||
EnvelopeId = notification.EnvelopeId,
|
||||
UserReference = notification.Receiver.EmailAddress,
|
||||
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||
UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress,
|
||||
Status = EnvelopeStatus.DocumentSigned,
|
||||
}, cancel);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
||||
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
||||
{
|
||||
emailOut.ReferenceString = notification.EmailAddress;
|
||||
emailOut.ReferenceId = notification.ReceiverId;
|
||||
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,11 +42,11 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
||||
{
|
||||
var placeHolders = new Dictionary<string, string>()
|
||||
{
|
||||
{ "[NAME_RECEIVER]", notification.Name ?? string.Empty },
|
||||
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
|
||||
{ "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? 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["[DOCUMENT_PROCESS]"] = string.Empty;
|
||||
|
||||
@@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using QRCoder;
|
||||
using System.Reflection;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||
|
||||
namespace EnvelopeGenerator.Application;
|
||||
|
||||
@@ -56,6 +59,22 @@ public static class DependencyInjection
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public record CreateEnvelopeReceiverCommand : CreateEnvelopeCommand, IRequest<Cr
|
||||
/// <param name="X">X-Position</param>
|
||||
/// <param name="Y">Y-Position</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>
|
||||
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
|
||||
@@ -41,7 +41,7 @@ public class ReceiverGetOrCreateCommand
|
||||
/// Unterschriften auf Dokumenten.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<Signature> Signatures { get; init; } = new();
|
||||
public List<DocReceiverElementCreateDto> DocReceiverElements { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Der Name, mit dem der Empfänger angesprochen werden soll.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Application;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Services;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace EnvelopeGenerator.Application.Services;
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Use MediatR")]
|
||||
public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, SignatureDto, Signature, int>, IDocumentReceiverElementService
|
||||
public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, DocReceiverElementDto, DocReceiverElement, int>, IDocumentReceiverElementService
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
|
||||
@@ -164,7 +164,7 @@ Public Class frmFinalizePDF
|
||||
Return
|
||||
End If
|
||||
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
|
||||
@@ -46,7 +46,7 @@ Namespace Jobs.FinalizeDocument
|
||||
Return pSourceBuffer
|
||||
End If
|
||||
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
@@ -58,7 +58,7 @@ Namespace Jobs.FinalizeDocument
|
||||
End Using
|
||||
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
|
||||
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
|
||||
'TODO: take the length from the largest y
|
||||
|
||||
@@ -10,8 +10,8 @@ Public Class ElementModel
|
||||
MyBase.New(pState)
|
||||
End Sub
|
||||
|
||||
Private Function ToElement(pRow As DataRow) As Signature
|
||||
Return New Signature() With {
|
||||
Private Function ToElement(pRow As DataRow) As DocReceiverElement
|
||||
Return New DocReceiverElement() With {
|
||||
.Id = pRow.ItemEx("GUID", 0),
|
||||
.DocumentId = pRow.ItemEx("DOCUMENT_ID", 0),
|
||||
.ReceiverId = pRow.ItemEx("RECEIVER_ID", 0),
|
||||
@@ -24,7 +24,7 @@ Public Class ElementModel
|
||||
}
|
||||
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).
|
||||
Select(AddressOf ToElement).
|
||||
ToList()
|
||||
@@ -80,7 +80,7 @@ Public Class ElementModel
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Public Function List(pDocumentId As Integer) As List(Of Signature)
|
||||
Public Function List(pDocumentId As Integer) As List(Of DocReceiverElement)
|
||||
Try
|
||||
Dim oSql = $"SELECT * FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE DOCUMENT_ID = {pDocumentId} ORDER BY PAGE ASC"
|
||||
Dim oTable = Database.GetDatatable(oSql)
|
||||
@@ -93,7 +93,7 @@ Public Class ElementModel
|
||||
End Try
|
||||
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
|
||||
Dim oReceiverConstraint = ""
|
||||
If pReceiverId > 0 Then
|
||||
@@ -111,7 +111,7 @@ Public Class ElementModel
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Public Function Insert(pElement As Signature) As Boolean
|
||||
Public Function Insert(pElement As DocReceiverElement) As Boolean
|
||||
Try
|
||||
Dim oSql = "INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
||||
([DOCUMENT_ID]
|
||||
@@ -161,7 +161,7 @@ Public Class ElementModel
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Public Function Update(pElement As Signature) As Boolean
|
||||
Public Function Update(pElement As DocReceiverElement) As Boolean
|
||||
Try
|
||||
Dim oSql = "UPDATE [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
||||
SET [POSITION_X] = @POSITION_X
|
||||
@@ -185,7 +185,7 @@ Public Class ElementModel
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Private Function GetElementId(pElement As Signature) As Integer
|
||||
Private Function GetElementId(pElement As DocReceiverElement) As Integer
|
||||
Try
|
||||
Return Database.GetScalarValue($"SELECT MAX(GUID) FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT
|
||||
WHERE DOCUMENT_ID = {pElement.DocumentId} AND RECEIVER_ID = {pElement.ReceiverId}")
|
||||
@@ -196,7 +196,7 @@ Public Class ElementModel
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Public Function DeleteElement(pElement As Signature) As Boolean
|
||||
Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
|
||||
Try
|
||||
Dim oSql = $"DELETE FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT WHERE GUID = {pElement.Id}"
|
||||
Return Database.ExecuteNonQuery(oSql)
|
||||
|
||||
@@ -11,9 +11,9 @@ using System.Collections.Generic;
|
||||
namespace EnvelopeGenerator.Domain.Entities
|
||||
{
|
||||
[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
|
||||
#if NETFRAMEWORK
|
||||
@@ -126,5 +126,33 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
[NotMapped]
|
||||
public double Left => Math.Round(X, 5);
|
||||
#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; }
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
public Document()
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
Elements = Enumerable.Empty<Signature>().ToList();
|
||||
Elements = Enumerable.Empty<DocReceiverElement>().ToList();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
FileNameOriginal { get; set; }
|
||||
#endregion
|
||||
|
||||
public virtual List<Signature>
|
||||
public virtual List<DocReceiverElement>
|
||||
#if nullable
|
||||
?
|
||||
#endif
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
ChangedWho { get; set; }
|
||||
|
||||
[ForeignKey("ElementId")]
|
||||
public virtual Signature
|
||||
public virtual DocReceiverElement
|
||||
#if nullable
|
||||
?
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace EnvelopeGenerator.Domain.Interfaces
|
||||
{
|
||||
public interface ISignature
|
||||
public interface IDocReceiverElement
|
||||
{
|
||||
int Page { get; set; }
|
||||
|
||||
@@ -8,7 +8,7 @@ Public Class FieldEditorController
|
||||
Inherits BaseController
|
||||
|
||||
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 ReceiverId As Integer
|
||||
@@ -36,7 +36,7 @@ Public Class FieldEditorController
|
||||
}
|
||||
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.
|
||||
Where(Function(e)
|
||||
Return e.Left = CSng(Math.Round(pAnnotation.Left, 5)) And
|
||||
@@ -47,12 +47,12 @@ Public Class FieldEditorController
|
||||
Return oElement
|
||||
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()
|
||||
Return oElement
|
||||
End Function
|
||||
|
||||
Public Function GetElement(pAnnotation As AnnotationStickyNote) As Signature
|
||||
Public Function GetElement(pAnnotation As AnnotationStickyNote) As DocReceiverElement
|
||||
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
||||
|
||||
If oInfo.Guid = -1 Then
|
||||
@@ -85,7 +85,7 @@ Public Class FieldEditorController
|
||||
Else
|
||||
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
||||
|
||||
Elements.Add(New Signature() With {
|
||||
Elements.Add(New DocReceiverElement() With {
|
||||
.ElementType = Constants.ElementType.Signature,
|
||||
.Height = oAnnotationHeight,
|
||||
.Width = oAnnotationWidth,
|
||||
@@ -98,7 +98,7 @@ Public Class FieldEditorController
|
||||
End If
|
||||
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.
|
||||
Where(Function(e) e.Page <> pPage And e.ReceiverId <> pReceiverId).
|
||||
ToList()
|
||||
@@ -120,7 +120,7 @@ Public Class FieldEditorController
|
||||
All(Function(pResult) pResult = True)
|
||||
End Function
|
||||
|
||||
Public Function SaveElement(pElement As Signature) As Boolean
|
||||
Public Function SaveElement(pElement As DocReceiverElement) As Boolean
|
||||
Try
|
||||
If pElement.Id > 0 Then
|
||||
Return ElementModel.Update(pElement)
|
||||
@@ -134,11 +134,11 @@ Public Class FieldEditorController
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Public Function DeleteElement(pElement As Signature) As Boolean
|
||||
Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
|
||||
Try
|
||||
' Element aus Datenbank löschen
|
||||
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()
|
||||
Return True
|
||||
Else
|
||||
|
||||
@@ -280,7 +280,7 @@ Partial Public Class frmFieldEditor
|
||||
End If
|
||||
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 oPage = pElement.Page
|
||||
Dim oReceiver = Receivers.Where(Function(r) r.Id = pReceiverId).Single()
|
||||
|
||||
@@ -74,14 +74,14 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
services.AddSQLExecutor<Envelope>();
|
||||
services.AddSQLExecutor<Receiver>();
|
||||
services.AddSQLExecutor<Document>();
|
||||
services.AddSQLExecutor<Signature>();
|
||||
services.AddSQLExecutor<DocReceiverElement>();
|
||||
services.AddSQLExecutor<DocumentStatus>();
|
||||
|
||||
SetDapperTypeMap<Envelope>();
|
||||
SetDapperTypeMap<User>();
|
||||
SetDapperTypeMap<Receiver>();
|
||||
SetDapperTypeMap<Document>();
|
||||
SetDapperTypeMap<Signature>();
|
||||
SetDapperTypeMap<DocReceiverElement>();
|
||||
SetDapperTypeMap<DocumentStatus>();
|
||||
|
||||
services.AddScoped<IEnvelopeExecutor, EnvelopeExecutor>();
|
||||
|
||||
@@ -45,7 +45,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
public DbSet<Envelope> Envelopes { get; set; }
|
||||
|
||||
public DbSet<Signature> DocumentReceiverElements { get; set; }
|
||||
public DbSet<DocReceiverElement> DocumentReceiverElements { get; set; }
|
||||
|
||||
public DbSet<ElementAnnotation> DocumentReceiverElementAnnotations { get; set; }
|
||||
|
||||
@@ -154,7 +154,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
#endregion EnvelopeDocument
|
||||
|
||||
#region DocumentReceiverElement
|
||||
modelBuilder.Entity<Signature>()
|
||||
modelBuilder.Entity<DocReceiverElement>()
|
||||
.HasOne(dre => dre.Document)
|
||||
.WithMany(ed => ed.Elements)
|
||||
.HasForeignKey(dre => dre.DocumentId);
|
||||
@@ -196,7 +196,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
#endregion DocumentStatus
|
||||
|
||||
#region Annotation
|
||||
modelBuilder.Entity<Signature>()
|
||||
modelBuilder.Entity<DocReceiverElement>()
|
||||
.HasMany(signature => signature.Annotations)
|
||||
.WithOne(annot => annot.Element)
|
||||
.HasForeignKey(annot => annot.ElementId);
|
||||
@@ -217,7 +217,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
// TODO: call add trigger methods with attributes and reflection
|
||||
AddTrigger<Config>();
|
||||
AddTrigger<Signature>();
|
||||
AddTrigger<DocReceiverElement>();
|
||||
AddTrigger<DocumentStatus>();
|
||||
AddTrigger<EmailTemplate>();
|
||||
AddTrigger<Envelope>();
|
||||
|
||||
@@ -6,7 +6,7 @@ using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
namespace EnvelopeGenerator.Infrastructure.Repositories;
|
||||
|
||||
[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)
|
||||
{
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
#endregion
|
||||
|
||||
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)
|
||||
Page(signature.Page, page =>
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject SignatureService SignatureService
|
||||
@inject SignatureCacheService SignatureCacheService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||
@inject AppVersionService AppVersion
|
||||
@inject ILogger<EnvelopeViewer> logger
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
@@ -217,6 +219,20 @@
|
||||
<div class="pdf-toolbar__divider"></div>
|
||||
|
||||
@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">
|
||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-nav"
|
||||
@onclick="GoToPreviousSignature"
|
||||
@@ -503,7 +519,7 @@ EnvelopeReceiverDto? _envelopeReceiver;
|
||||
int _totalSignatures = 0;
|
||||
int _signedSignatures = 0;
|
||||
int _unsignedSignatures = 0;
|
||||
int _currentSignatureIndex = 0; // Şu an hangi imzada (1-based)
|
||||
int _currentSignatureIndex = 0; // Current signature index (1-based)
|
||||
|
||||
// Signature state
|
||||
SignatureCaptureDto? _capturedSignature;
|
||||
@@ -563,10 +579,34 @@ const int MaxThumbnailWidth = 400;
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures);
|
||||
|
||||
// Open signature popup on page load
|
||||
// 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;
|
||||
|
||||
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) {
|
||||
_errorMessage = $"Fehler: {ex.Message}";
|
||||
@@ -777,7 +817,7 @@ const int MaxThumbnailWidth = 400;
|
||||
_totalSignatures = state.Total;
|
||||
_signedSignatures = state.Signed;
|
||||
_unsignedSignatures = state.Unsigned;
|
||||
_currentSignatureIndex = state.CurrentIndex; // Şu an hangi imzada
|
||||
_currentSignatureIndex = state.CurrentIndex; // Current signature
|
||||
await InvokeAsync(StateHasChanged);
|
||||
} catch {
|
||||
// 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);
|
||||
|
||||
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
|
||||
void OpenSignaturePopup() {
|
||||
// Open popup with current signature (edit mode)
|
||||
_activeSignatureTab = SignatureTabDraw;
|
||||
_signaturePopupVisible = true;
|
||||
_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() {
|
||||
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) {
|
||||
@@ -881,6 +958,22 @@ const int MaxThumbnailWidth = 400;
|
||||
};
|
||||
_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);
|
||||
Console.WriteLine($"Signature saved: {_signerFullName}, {_signaturePlace}");
|
||||
}
|
||||
|
||||
@@ -16,12 +16,13 @@ builder.Services.Configure<ApiOptions>(opts =>
|
||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.DocumentService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AuthService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AnnotationService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.SignatureService>();
|
||||
builder.Services.AddSingleton<EnvelopeGenerator.ReceiverUI.Services.AppVersionService>();
|
||||
builder.Services.AddScoped<DocumentService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<AnnotationService>();
|
||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||
builder.Services.AddScoped<SignatureService>();
|
||||
builder.Services.AddScoped<SignatureCacheService>();
|
||||
builder.Services.AddSingleton<AppVersionService>();
|
||||
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,6 +484,68 @@ body.resizing {
|
||||
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 {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
|
||||
@@ -489,7 +489,7 @@ window.pdfViewer = {
|
||||
* @returns {object} { total, signed, unsigned, currentIndex, canGoPrev, canGoNext }
|
||||
*/
|
||||
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) {
|
||||
return {
|
||||
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;
|
||||
if (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 {
|
||||
total: total,
|
||||
signed: signed,
|
||||
unsigned: unsigned,
|
||||
currentIndex: currentIndex, // Şu an hangi imzada (1-5 arası)
|
||||
canGoPrev: total > 0, // Her zaman aktif (e?er imza varsa)
|
||||
canGoNext: total > 0 // Her zaman aktif (e?er imza varsa)
|
||||
currentIndex: currentIndex, // Current signature index (1-5 range)
|
||||
canGoPrev: total > 0, // Always active (if signatures exist)
|
||||
canGoNext: total > 0 // Always active (if signatures exist)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -529,63 +530,64 @@ window.pdfViewer = {
|
||||
* Cross-page navigation: searches ALL pages for next unsigned signature.
|
||||
*/
|
||||
async goToNextSignature(dotNetRef) {
|
||||
// Global imza listesi yoksa <20>?k
|
||||
// Exit if no global signature list
|
||||
if (!this._allSignatures || this._allSignatures.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mevcut g<>r<EFBFBD>nt<6E>lenen imzan?n index'ini bul
|
||||
// Find current displayed signature's index
|
||||
let currentIndex = -1;
|
||||
if (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;
|
||||
|
||||
// Sonsuz d<>ng<6E>: Son imzadaysa ilk imzaya d<>n
|
||||
// Infinite loop: If at last signature, return to first
|
||||
if (nextIndex >= this._allSignatures.length) {
|
||||
nextIndex = 0; // ?lk imzaya d<>n
|
||||
nextIndex = 0; // Return to first signature
|
||||
}
|
||||
|
||||
const nextSignature = this._allSignatures[nextIndex];
|
||||
|
||||
// Farkl? sayfadaysa sayfa de?i?tir
|
||||
// Change page if signature is on different page
|
||||
if (nextSignature.page !== this.pageNum) {
|
||||
// Sayfa de?i?tir
|
||||
// Change page
|
||||
this.pageNum = nextSignature.page;
|
||||
this.queueRenderPage(this.pageNum);
|
||||
|
||||
// Render tamamlanana kadar bekle
|
||||
// Wait until render completes
|
||||
let waitCount = 0;
|
||||
while (this.pageRendering && waitCount < 20) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
waitCount++;
|
||||
}
|
||||
|
||||
// Blazor'a haber ver - signature butonlar?n? yeniden ?iz
|
||||
// Notify Blazor - re-render signature buttons
|
||||
if (dotNetRef) {
|
||||
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));
|
||||
}
|
||||
|
||||
// Son g<>r<EFBFBD>nt<6E>lenen imzay? kaydet
|
||||
|
||||
// Save last viewed signature
|
||||
this._lastViewedSignatureId = nextSignature.id;
|
||||
|
||||
// ?mza imzalanm?? m? kontrol et
|
||||
// Check if signature is signed
|
||||
const isApplied = this.appliedSignatures.some(s => s.id === nextSignature.id);
|
||||
|
||||
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}"]`);
|
||||
if (container) {
|
||||
this.scrollToElement(container);
|
||||
}
|
||||
} else {
|
||||
// ?mzalanmam?? - butonu bul ve scroll yap
|
||||
// Unsigned - find button and scroll
|
||||
const button = this.signatureButtons.find(btn =>
|
||||
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) {
|
||||
dotNetRef.invokeMethodAsync('OnSignatureNavChanged');
|
||||
}
|
||||
@@ -611,58 +613,58 @@ window.pdfViewer = {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mevcut g<>r<EFBFBD>nt<6E>lenen imzan?n index'ini bul
|
||||
let currentIndex = this._allSignatures.length; // Varsay?lan: son imzadan sonra
|
||||
// Find current displayed signature's index
|
||||
let currentIndex = this._allSignatures.length; // Default: after last signature
|
||||
if (this._lastViewedSignatureId) {
|
||||
currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
|
||||
}
|
||||
|
||||
// Bir <20>nceki imzay? al
|
||||
// Get previous signature
|
||||
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) {
|
||||
prevIndex = this._allSignatures.length - 1; // Son imzaya git
|
||||
prevIndex = this._allSignatures.length - 1; // Go to last signature
|
||||
}
|
||||
|
||||
const prevSignature = this._allSignatures[prevIndex];
|
||||
|
||||
// Change page if needed
|
||||
if (prevSignature.page !== this.pageNum) {
|
||||
// Sayfa de?i?tir
|
||||
// Change page
|
||||
this.pageNum = prevSignature.page;
|
||||
this.queueRenderPage(this.pageNum);
|
||||
|
||||
// Render tamamlanana kadar bekle
|
||||
// Wait until render completes
|
||||
let waitCount = 0;
|
||||
while (this.pageRendering && waitCount < 20) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
waitCount++;
|
||||
}
|
||||
|
||||
// Blazor'a haber ver - signature butonlar?n? yeniden <20>iz
|
||||
// Notify Blazor - re-render signature buttons
|
||||
if (dotNetRef) {
|
||||
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
|
||||
}
|
||||
|
||||
// DOM g<>ncellenmesini bekle
|
||||
// Wait for DOM update
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
}
|
||||
|
||||
// Son g<>r<EFBFBD>nt<6E>lenen imzay? kaydet
|
||||
// Save last viewed signature
|
||||
this._lastViewedSignatureId = prevSignature.id;
|
||||
|
||||
// ?mza imzalanm?? m? kontrol et
|
||||
// Check if signature is signed
|
||||
const isApplied = this.appliedSignatures.some(s => s.id === prevSignature.id);
|
||||
|
||||
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}"]`);
|
||||
if (container) {
|
||||
this.scrollToElement(container);
|
||||
}
|
||||
} else {
|
||||
// ?mzalanmam?? - butonu bul ve scroll yap
|
||||
// Unsigned - find button and scroll
|
||||
const button = this.signatureButtons.find(btn =>
|
||||
parseInt(btn.getAttribute('data-signature-id')) === prevSignature.id
|
||||
);
|
||||
@@ -1070,8 +1072,8 @@ window.pdfViewer = {
|
||||
|
||||
// Text information container
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'signature-info-text'; // ✅ Class ekle (querySelector için)
|
||||
infoContainer.setAttribute('data-base-font-size', '9'); // ✅ Base font size sakla
|
||||
infoContainer.className = 'signature-info-text'; // ✅ Add class (for querySelector)
|
||||
infoContainer.setAttribute('data-base-font-size', '9'); // ✅ Store base font size
|
||||
infoContainer.style.fontSize = '9px';
|
||||
infoContainer.style.lineHeight = '1.4';
|
||||
infoContainer.style.color = '#495057';
|
||||
|
||||
@@ -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 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 ??????????????????????????????????????????????????????????
|
||||
return {
|
||||
startTyped: startTyped,
|
||||
@@ -329,6 +346,8 @@ window.receiverSignature = (() => {
|
||||
renderTypedSignature: renderTypedSignature,
|
||||
getDataUrl: getDataUrl,
|
||||
getTypedDataUrl: getTypedDataUrl,
|
||||
getImageDataUrl: getImageDataUrl
|
||||
getImageDataUrl: getImageDataUrl,
|
||||
loadExistingSignature: loadExistingSignature
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Infrastructure;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
@@ -53,7 +54,7 @@ public class DocSignedNotificationTests : TestBase
|
||||
|
||||
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
|
||||
|
||||
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
|
||||
var docSignedNtf = new DocSignedNotification { EnvelopeReceiver = envRcvDto, PsPdfKitAnnotation = annots };
|
||||
|
||||
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
@@ -45,6 +47,7 @@ public class AnnotationController : ControllerBase
|
||||
|
||||
[Authorize(Roles = Role.ReceiverFull)]
|
||||
[HttpPost]
|
||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||
{
|
||||
// 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))
|
||||
return Problem(statusCode: 423);
|
||||
|
||||
var docSignedNotification = await _mediator
|
||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
||||
var docSignedNotification = envelopeReceiverDto is not null
|
||||
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
||||
: 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);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
namespace EnvelopeGenerator.Web.Controllers.Test;
|
||||
|
||||
[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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user