Compare commits

..

31 Commits

Author SHA1 Message Date
OlgunR
eeb2ced141 Test push für Marvin No. 6 2026-03-31 12:04:32 +02:00
OlgunR
6f9c0e1bde Test push für Marvin No. 5 2026-03-31 12:00:26 +02:00
OlgunR
716774391c Update assembly info: version, copyright, trademark
Incremented assembly version to 2.9.3.0. Updated copyright year to 2026 and cleared the trademark field in AssemblyInfo.vb.
2026-03-11 13:33:52 +01:00
OlgunR
e72cfcb632 Add comment for status translation in EnvelopeHistory
Added a developer note in EnvelopeHistory.cs to indicate that status translation should be implemented similarly to the envelope entity. No functional changes were made.
2026-03-11 13:15:30 +01:00
OlgunR
f0f96e45fd Fix typo in DocumentSigned4RaC resource string
Corrected "red and confirmed" to "read and confirmed" in Model.en.resx.
2026-03-11 10:37:42 +01:00
OlgunR
a2a568480c Update column captions to "Processed on" in frmMain.resx
Changed "Abgeschlossen am" to "Bearbeitet am" for ColSignedDateCompleted and ColSignedDate captions to better reflect the status as "Processed on" instead of "Completed on".
2026-03-10 14:59:49 +01:00
OlgunR
23f67029bd Update column captions to "Completed on" in frmMain.resx
Changed captions for "ColSignedDateCompleted" and "ColSignedDate" from "Signed on" to "Completed on" to better reflect the data shown.
2026-03-10 14:51:19 +01:00
OlgunR
d29abf53e7 Set envelope default message by type using resources
Added "Please read and confirm this document" to resource files and updated resource class. Replaced Envelope constructor with WithDefaultMessage extension method to set default message based on EnvelopeTypeId. Updated controller and editor form to use this method. Cleaned up imports and comments.
2026-03-06 14:23:18 +01:00
OlgunR
967fd2ba04 Set Message default via ctor for .NET Framework only
Removed default value assignment from Message property and added a constructor to set the default message only when compiling for .NET Framework, using conditional compilation. This improves cross-platform compatibility.
2026-03-06 11:16:27 +01:00
OlgunR
f8ce9a03ec Update envelope prompts for clarity and consistency
Revised resource strings in English, French, and German to generalize the "start process" prompt and update "Envelope Invitations Sent" to refer to the envelope dispatch. Updated designer file comments to match new localized text. These changes improve clarity and consistency across all supported languages.
2026-03-06 11:03:05 +01:00
OlgunR
4335a500f1 Update envelope type 2 confirmation message
Revised the message for envelope type 2 to improve clarity. The new text now asks users to confirm they have read the document, removing the previous instruction to read and confirm.
2026-03-04 09:33:29 +01:00
OlgunR
0e2c653784 Refactor SignatureLinkText to DocumentProcess property
Replaced SignatureLinkText with DocumentProcess in EmailData and updated all references. Adjusted DynamicStringsForEmails logic and template replacements accordingly. Added debug logging for template replacements. Bumped assembly version to 2.7.1.0.
2026-03-04 09:30:29 +01:00
OlgunR
92d88812e8 Add SignatureLinkText to EmailData and template support
Introduced SignatureLinkText property in EmailData to hold signature-related text, set dynamically based on envelope type. Updated constructor to HTML-encode SignatureLinkText. Added [SIGNATURE_LINK_TEXT] to template dictionary in TemplateService for email customization.
2026-03-03 12:44:54 +01:00
OlgunR
af60dfe338 Refactor email content logic into EmailData class
Move dynamic message and signature type logic from TemplateService to EmailData via DynamicStringsForEmails. Constructors now set and HTML-encode these properties. Improves maintainability by centralizing email content generation.
2026-03-03 10:42:18 +01:00
OlgunR
a7fff24f80 Add EnvelopeTypeId to EmailData and dynamic signature type
Introduced EnvelopeTypeId as a read-only property in EmailData, set during construction. Updated TemplateService.InitDictionary to use EnvelopeTypeId for determining signature type and message content, replacing the hardcoded [SIGNATURE_TYPE] value. Also added necessary imports for domain entities and constants.
2026-03-03 09:19:18 +01:00
OlgunR
9b70ca8ce6 Refactor envelope status translation logic
Simplified the logic for translating envelope status by using pattern matching to directly return the "Read and Sign" translation if available, otherwise falling back to the default. Also renamed the resource manager variable (note: introduced a typo as "rescourceManager").
2026-02-25 11:30:22 +01:00
OlgunR
66c1fb3698 Replace ContractType.ReadAndSign with literal value 2
Updated envelope validation to compare EnvelopeTypeId directly to the integer 2 instead of using the ContractType.ReadAndSign constant. This simplifies the condition and removes dependency on the constant.
2026-02-24 11:30:26 +01:00
OlgunR
8766b9e386 Add RaC resource strings and update localization for statuses
Introduce new "4RaC" resource strings for "Read and Confirm" workflow in both English and German resource files, with corresponding C# accessors. Update existing resource values and comments for clarity, remove unused "EnvelopeSaved4RaC" accessor, and ensure consistency across resource entries.
2026-02-24 11:14:13 +01:00
OlgunR
9419cb69ad Refactor StatusTranslated property for better fallback logic
Refactored the StatusTranslated property to first attempt fetching the translation with the "4RaC" suffix when IsReadAndSign() is true, and only use it if a translation exists. If not, or if IsReadAndSign() is false, it falls back to the standard status translation. This improves clarity and ensures a fallback translation is always provided.
2026-02-24 11:13:46 +01:00
OlgunR
3250f36c1f Add RaC resource strings and update comments to English
Added DocumentSigned4RaC and EnvelopeSaved4RaC resource strings with English and German translations. Updated Model.en.resx to include PartlySigned translation. Translated all resource accessor class comments from German to English for clarity.
2026-02-23 16:03:19 +01:00
OlgunR
887d76e2fc Add IsReadAndSign and enhance StatusTranslated logic
Refactored StatusTranslated to append "4RaC" to the resource key for "Read and Sign" envelopes (EnvelopeTypeId == 2). Introduced an IsReadAndSign extension method for Envelope. Added necessary namespace import for extension support.
2026-02-23 16:03:09 +01:00
OlgunR
f303ba042b Refactor contract type handling in EnvelopeType entity
Replaced Constants.ContractType enum with integer property in EnvelopeType. Updated related references in EnvelopeEditorController. Removed EnvelopeType variable from ModuleSettings. Made EnvelopeType property bindable and initialized in frmEnvelopeMainData to improve consistency and simplify contract type management.
2026-02-19 13:57:31 +01:00
OlgunR
ae33254743 Refactor envelope type handling and validation logic
- Change ContractType property to use Constants.ContractType enum for improved type safety.
- Move EnvelopeType selection to a shared module-level variable for consistent access.
- Allow user selection of contract type when creating envelopes.
- Refine envelope validation to check for required elements per receiver, with improved error messages.
2026-02-19 13:37:58 +01:00
OlgunR
341713d4cb Replace resource strings with hardcoded English messages
All user-facing messages previously using resource strings have been changed to hardcoded English text. This affects MsgBox dialogs, Worker.ReportProgress updates, and exception messages, removing localization support and standardizing messages to plain English.
2026-02-18 08:48:52 +01:00
OlgunR
f1b34dfa42 Use localized status messages from Resources.Envelope
Replaced hardcoded status messages with resource-based strings for "Initializing Configuration" and "Starting Application" to improve internationalization and maintainability.
2026-02-17 16:46:43 +01:00
OlgunR
676598450d Translate UI into English and french; Extend resources and outsource hardcoded String form codebase into resources. 2026-02-16 15:05:54 +01:00
Developer01
c0b2ad0d9b Push nach rename 2026-02-10 08:42:14 +01:00
Developer01
2b7d797c3e 2.9.1 2026-02-10 08:38:45 +01:00
Developer01
1e2e018f7f MS Anpassungen 2025-11-12 21:43:50 +01:00
Developer01
69afea012b Handling InsertReceivers und kleinigkeiten frm Main 2025-09-16 09:00:43 +02:00
Developer01
85e0bc3400 Logging wegen Y-Konstante 2025-09-04 14:35:54 +02:00
746 changed files with 27053 additions and 48785 deletions

View File

@@ -1,424 +0,0 @@
# EnvelopeGenerator — AI Context Reference
## Purpose
Digital document signing system with **unified Blazor WASM frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
---
## Deployment Architecture
**Two Presentation Projects (Both Required):**
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
- Runs independently (development & production)
- **YARP Reverse Proxy** configured via `yarp.json`
- Proxies requests to:
- `EnvelopeGenerator.ReceiverUI` (Blazor WASM)
- External Auth.API service
- Serves as single entry point for all requests
2. **EnvelopeGenerator.ReceiverUI** (Blazor WebAssembly)
- Runs on separate host/port
- Accessed **only through API proxy** (not directly)
- Serves static files (HTML, JS, CSS, WASM)
**Request Flow:**
```
Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
? Auth.API:9090 (External Auth Service)
```
**Configuration:** `EnvelopeGenerator.API/yarp.json`
---
## ReceiverUI Route Structure
### Root Route
| Route | File | Purpose |
|---|---|---|
| `/` | `Index.razor` | Application entry point (landing page). |
### Sender Routes
| Route | File | Purpose |
|---|---|---|
| `/sender/login` | `LoginSenderPage.razor` | Username/password authentication |
| `/sender` | `EnvelopeSenderPage.razor` | Sender dashboard (envelope list) |
### Receiver Routes
| Route | File | Purpose |
|---|---|---|
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | Access code authentication for specific envelope |
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | View & sign envelope (PDF.js viewer) |
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
---
## Architecture Evolution
### Old Architecture (Deprecated)
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
- **Receiver UI:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM + PDF.js)
- **Backend:** `EnvelopeGenerator.API`
### Current Architecture
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM) — **Both Senders & Receivers**
- **Backend:** `EnvelopeGenerator.API`**Both Senders & Receivers**
- **Libraries:** DevExpress + PDF.js
- **PSPDFKit:** **REMOVED**
---
## Solution Structure
| Project | Target | Purpose |
|---|---|---|
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **Unified Blazor WebAssembly Frontend**. UI for **both Senders & Receivers**. YARP proxy to API. |
| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in ReceiverUI). |
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
---
## Key Files & Routes
| File | Route/Purpose |
|---|---|
| `ReceiverUI/Pages/Index.razor` | `/` — Application entry point (landing page). |
| `ReceiverUI/Pages/EnvelopeSenderPage.razor` | `/sender` — Sender dashboard (envelope list). |
| `ReceiverUI/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` — Receiver PDF viewer & signing. |
| `ReceiverUI/Pages/LoginSenderPage.razor` | `/sender/login` — Sender username/password auth. |
| `ReceiverUI/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` — Receiver access code auth. |
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
| `ReceiverUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
| `ReceiverUI/Services/AuthService.cs` | Receiver + Sender authentication. |
| `ReceiverUI/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
---
## Coordinate System — CRITICAL
**Database Format:** INCHES (GdPicture14 native)
**Origin:** Top-left corner
**Axes:** X right, Y down
### Conversion Formulas
| From INCHES to | Formula | Example |
|---|---|---|
| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX |
| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt |
| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` |
**A4 Dimensions:**
- Width: 8.27" = 595pt = 827 DX
- Height: 11.69" = 842pt = 1169 DX
### Unit Systems
| System | Unit | Origin | Y-Axis |
|---|---|---|---|
| **Database (GdPicture14)** | Inches | Top-left | Down |
| PDF.js | Pixels | Top-left | Down |
| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) |
| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** |
---
## EnvelopeReceiver — PDF.js Viewer & Signing
**Route:** `/envelope/{EnvelopeKey}`
**Tech:** PDF.js 3.11.174 + Blazor WASM + configurable quality
**File:** `ReceiverUI/Pages/EnvelopeReceiverPage.razor`
### Key Features
1. HiDPI/Retina support (4x quality)
2. Configurable quality (`appsettings.json`)
3. Unlimited zoom (50%-300%)
4. Ctrl+Wheel global zoom
5. Resizable thumbnail sidebar (150-400px, localStorage)
6. Responsive (desktop/mobile)
### Configuration
**File:** `ReceiverUI/wwwroot/appsettings.json`
```json
{
"PdfViewer": {
"ThumbnailBaseScale": 0.75,
"ThumbnailEnableHiDPI": true,
"MainCanvasEnableHiDPI": true,
"ZoomStepPercentage": 5
}
}
```
### JavaScript API
**File:** `ReceiverUI/wwwroot/js/pdf-viewer.js`
```javascript
window.pdfViewer = {
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
renderSignatureButtons(signatures, pageNum, dotNetRef),
applySignature(signatureId, dataUrl, fullName, position, place),
zoomIn(), zoomOut(), dispose()
}
```
---
## Signature Workflow — EnvelopeReceiver
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
### Workflow Steps
1. **Page Load:**
- Check `SignatureCacheService` for cached signature
- If cached ? skip popup, load signature
- If not ? show automatic popup (mandatory)
2. **Signature Popup (DxPopup):**
- **Cannot close** (no X, no ESC, no outside-click)
- **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload)
- **Required:** Full name, Place
- **Optional:** Position
- **Save ?** Store in `_capturedSignature`, cache via API
3. **Signature Buttons:**
- Render purple "Unterschreiben" buttons at signature field positions
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
- File: `pdf-viewer.js` ? `renderSignatureButtons()`
4. **Apply Signature (Click "Unterschreiben"):**
- JS: Remove button, create HTML overlay
- Format: Image + separator + text (Name, Position, Place, Date)
- **NOT stamped on PDF bytes** (visual overlay only)
5. **Re-rendering:**
- Zoom/Page change ? recalculate button positions
- Session state: `_capturedSignature` (lost on refresh)
### Data Model
**File:** `ReceiverUI/Models/SignatureCaptureDto.cs`
```csharp
public sealed record SignatureCaptureDto {
public required string DataUrl { get; init; } // base64 PNG
public required string FullName { get; init; }
public string Position { get; init; } = ""; // Optional
public required string Place { get; init; }
}
```
---
## Signature Caching
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
### API Endpoints
**Controller:** `API/Controllers/CacheController.cs`
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
**Cache Key Format:**
```
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
```
**Configuration:** `appsettings.json`
```json
{
"Cache": {
"SignatureCacheExpiration": null // or "02:00:00" for 2h
}
}
```
### Service
**File:** `ReceiverUI/Services/SignatureCacheService.cs`
```csharp
public class SignatureCacheService {
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
Task DeleteSignatureAsync(string envelopeKey);
}
```
**Error Handling:** Fire-and-forget saves, graceful degradation on load failure.
---
## Sender Login
**Route:** `/sender/login`
**File:** `ReceiverUI/Pages/LoginSenderPage.razor`
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
### AuthService Extension
**File:** `ReceiverUI/Services/AuthService.cs`
```csharp
public enum SenderLoginResult { Success, InvalidCredentials, Error }
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/auth?cookie=true",
new { username, password });
return response.StatusCode switch {
HttpStatusCode.OK => SenderLoginResult.Success,
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
_ => SenderLoginResult.Error
};
}
```
### API Integration
**Endpoint:** `POST /api/auth?cookie=true`
**Request:**
```json
{ "username": "TekH", "password": "***" }
```
**Response:**
- `200 OK` ? Cookie set, redirect to `/sender`
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
- Other ? Show error: "Serverfehler"
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
### UI Flow
1. User enters username + password
2. Click "Anmelden" or press Enter
3. Call `AuthService.LoginSenderAsync()`
4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)`
5. Error ? Display alert
---
## Receiver Login
**Route:** `/envelope/login/{EnvelopeKey}`
**File:** `ReceiverUI/Pages/LoginReceiverPage.razor`
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
### AuthService Method
```csharp
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
var form = new MultipartFormDataContent();
form.Add(new StringContent(accessCode), "AccessCode");
var response = await http.PostAsync(
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
return response.StatusCode switch {
HttpStatusCode.OK => EnvelopeLoginResult.Success,
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
_ => EnvelopeLoginResult.Error
};
}
```
**Success:** Redirect to `/envelope/{key}`
---
## NuGet Packages (ReceiverUI)
| Package | Version | Purpose |
|---|---|---|
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
**External CDN:**
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
---
## Mistakes History — Do NOT Repeat
| Mistake | Why Wrong |
|---|---|
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
---
## Development Notes
### Deprecated Projects
**DO NOT USE:**
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified ReceiverUI
- PSPDFKit — Removed, use PDF.js + DevExpress instead
### Legacy Projects (VB.NET)
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
### Signature Coordinate Evidence
**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET)
```vb
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
Sub LoadAnnotation(pElement As Signature, ...)
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
oAnnotation.Top = CSng(pElement.Y)
End Sub
```
Proves database uses INCHES natively.
---
## Quick Reference
### When working with coordinates:
1. **Database ? UI:** INCHES × 72 = PDF Points
2. **UI ? Display:** Points × scale = Pixels
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
### When adding features:
1. Check `Mistakes History` first
2. Prefer simplicity over complexity
3. Use `appsettings.json` for configuration
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
5. **Unified frontend:** ReceiverUI serves both Senders and Receivers
### When debugging:
1. **Coordinates:** Always check unit system (inches/points/pixels)
2. **Authentication:** Check cookie name/domain/SameSite
3. **Cache:** Check Redis/SQL connection + key format
4. **Frontend confusion:** Only use ReceiverUI (Web is deprecated)
---
**Last Updated:** Session 19 (Razor file naming convention + Index route proxy)

View File

@@ -1,17 +0,0 @@
namespace EnvelopeGenerator.API;
/// <summary>
///
/// </summary>
public static class AuthScheme
{
/// <summary>
/// Scheme name used for per-envelope receiver JWT authentication.
/// </summary>
public const string Receiver = "EnvelopeGenerator.API.ReceiverJWT";
/// <summary>
/// Scheme name used for per-envelope sender JWT authentication.
/// </summary>
public const string Sender = "EnvelopeGenerator.API.SenderJWT";
}

View File

@@ -1,132 +0,0 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.API.Extensions;
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;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Manages annotations and signature lifecycle for envelopes.
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[ApiController]
[Route("api/[controller]")]
public class AnnotationController : ControllerBase
{
[Obsolete("Use MediatR")]
private readonly IEnvelopeHistoryService _historyService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverService _envelopeReceiverService;
private readonly IMediator _mediator;
private readonly ILogger<AnnotationController> _logger;
/// <summary>
/// Initializes a new instance of <see cref="AnnotationController"/>.
/// </summary>
[Obsolete("Use MediatR")]
public AnnotationController(
ILogger<AnnotationController> logger,
IEnvelopeHistoryService envelopeHistoryService,
IEnvelopeReceiverService envelopeReceiverService,
IMediator mediator)
{
_historyService = envelopeHistoryService;
_envelopeReceiverService = envelopeReceiverService;
_mediator = mediator;
_logger = logger;
}
/// <summary>
/// Creates or updates annotations for the authenticated envelope receiver.
/// </summary>
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
/// <param name="cancel">Cancellation token.</param>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpPost]
[Obsolete("PSPDF Kit will no longer be used.")]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{
var signature = User.ReceiverSignature();
var uuid = User.EnvelopeUuid();
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
return BadRequest();
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
return Problem(statusCode: StatusCodes.Status409Conflict);
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
return Problem(statusCode: StatusCodes.Status423Locked);
var 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.");
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();
}
/// <summary>
/// Rejects the document for the current receiver.
/// </summary>
/// <param name="reason">Optional rejection reason.</param>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpPost("reject")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> Reject([FromBody] string? reason = null)
{
var signature = User.ReceiverSignature();
var uuid = User.EnvelopeUuid();
var mail = User.ReceiverMail();
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
if (envRcvRes.IsFailed)
{
_logger.LogNotice(envRcvRes.Notices);
return Unauthorized("you are not authorized");
}
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
if (histRes.IsSuccess)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return NoContent();
}
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
_logger.LogNotice(histRes.Notices);
return StatusCode(500, histRes.Messages);
}
}

View File

@@ -1,117 +0,0 @@
using DigitalData.Auth.Claims;
using EnvelopeGenerator.API.Controllers.Interfaces;
using EnvelopeGenerator.API.Models;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions, IAuthorizationService authService) : ControllerBase, IAuthController
{
private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value;
/// <summary>
///
/// </summary>
public IAuthorizationService AuthService { get; } = authService;
/// <summary>
/// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie)
/// </summary>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth/logout
///
/// </remarks>
/// <response code="200">Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat.</response>
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
Response.Cookies.Delete(authTokenKeys.Cookie);
else if (await this.IsUserInPolicyAsync(AuthPolicy.ReceiverOrReceiverTFA))
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
else
return Unauthorized();
return Ok();
}
/// <summary>
/// Prüft, ob der Benutzer ein autorisiertes Token hat.
/// </summary>
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
/// <remarks>
/// Sample request:
///
/// GET /api/auth
///
/// </remarks>
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[HttpGet("check")]
[Authorize]
public IActionResult Check(string? role = null)
=> role is not null && !User.IsInRole(role)
? Unauthorized()
: Ok();
/// <summary>
/// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key.
/// The request must carry a cookie named <c>AuthTokenSignFLOWReceiver.{envelopeKey}</c>.
/// </summary>
/// <param name="envelopeKey">The unique envelope key extracted from the route.</param>
/// <response code="200">Valid per-envelope token found.</response>
/// <response code="401">Token is missing, expired or invalid.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("check/envelope/{envelopeKey}")]
public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok();
/// <summary>
/// Removes the per-envelope receiver cookie for the given envelope key.
/// </summary>
/// <param name="envelopeKey">The unique envelope key whose cookie should be deleted.</param>
/// <response code="200">Cookie successfully deleted.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[HttpPost("logout/envelope/{envelopeKey}")]
public IActionResult LogoutEnvelopeReceiver([FromRoute] string envelopeKey)
{
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
Response.Cookies.Delete(cookieName);
return Ok();
}
/// <summary>
/// Removes all per-envelope receiver cookies from the current request.
/// </summary>
/// <response code="200">All envelope receiver cookies successfully deleted.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[HttpPost("logout/envelope")]
public IActionResult LogoutAllEnvelopeReceivers()
{
foreach (var cookieName in Request.Cookies.Keys.Where(k => CookieNames.IsEnvelopeReceiverCookie(k, authTokenKeys.Cookie)))
Response.Cookies.Delete(cookieName);
return Ok();
}
}

View File

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

View File

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

View File

@@ -1,84 +0,0 @@
using DigitalData.Auth.Claims;
using EnvelopeGenerator.API.Controllers.Interfaces;
using EnvelopeGenerator.API.Extensions;
using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Provides access to envelope documents for authenticated receivers.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="DocumentController"/> class.
/// </remarks>
[ApiController]
[Route("api/[controller]")]
public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger<DocumentController> logger) : ControllerBase, IAuthController
{
/// <summary>
///
/// </summary>
public IAuthorizationService AuthService => authService;
/// <summary>
/// Returns the document bytes receiver.
/// </summary>
/// <param name="query">Encoded envelope key.</param>
/// <param name="cancel">Cancellation token.</param>
[HttpGet]
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
{
// Sender: expects query with envelope key
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
{
if (query is null)
return BadRequest("Missing document query.");
var senderDoc = await mediator.Send(query, cancel);
return senderDoc.ByteData is byte[] senderDocByte
? File(senderDocByte, "application/octet-stream")
: NotFound("Document is empty.");
}
// Receiver: resolve envelope id from claims
if (await this.IsUserInPolicyAsync(AuthPolicy.Receiver))
{
if (query is not null)
return BadRequest("Query parameters are not allowed for receiver role.");
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")
: NotFound("Document is empty.");
}
return Unauthorized();
}
/// <summary>
/// Gets the document for the specified envelope key.
/// </summary>
/// <param name="envelopeKey"></param>
/// <param name="cancel"></param>
/// <returns></returns>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("{envelopeKey}")]
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
{
int envelopeId = User.EnvelopeId();
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
if (senderDoc.ByteData is not byte[] senderDocByte)
return NotFound("Document is empty.");
Response.Headers.ContentDisposition = $"inline; filename=\"{envelopeKey}.pdf\"";
return File(senderDocByte, "application/pdf");
}
}

View File

@@ -1,69 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Application.EmailTemplates.Commands;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using EnvelopeGenerator.Application.Common.Dto;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.EmailTemplates.Queries;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Controller for managing temp templates.
/// Steuerung zur Verwaltung von E-Mail-Vorlagen.
/// </summary>
/// <remarks>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </remarks>
/// <param name="mediator">
/// Die Mediator-Instanz, die zum Senden von Befehlen und Abfragen verwendet wird.
/// </param>
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = AuthPolicy.Sender)]
public class EmailTemplateController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Ruft E-Mail-Vorlagen basierend auf der angegebenen Abfrage ab.
/// Gibt alles zurück, wenn keine Id- oder Typ-Informationen eingegeben wurden.
/// </summary>
/// <param name="emailTemplate">Die Abfrageparameter zum Abrufen von E-Mail-Vorlagen.</param>
/// <param name="cancel"></param>
/// <returns>Gibt HTTP-Antwort zurück</returns>
/// <remarks>
/// Sample request:
/// GET /api/EmailTemplate?emailTemplateId=123
/// </remarks>
/// <response code="200">Wenn die E-Mail-Vorlagen erfolgreich abgerufen werden.</response>
/// <response code="400">Wenn die Abfrageparameter ungültig sind.</response>
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
[HttpGet]
public async Task<IActionResult> Get([FromQuery] ReadEmailTemplateQuery emailTemplate, CancellationToken cancel)
{
var result = await mediator.Send(emailTemplate, cancel);
return Ok(result);
}
/// <summary>
/// Updates an temp template or resets it if no update command is provided.
/// Aktualisiert eine E-Mail-Vorlage oder setzt sie zurück, wenn kein Aktualisierungsbefehl angegeben ist.
/// </summary>
/// <param name="update"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <response code="200">Wenn die E-Mail-Vorlage erfolgreich aktualisiert oder zurückgesetzt wird.</response>
/// <response code="400">Wenn die Abfrage ohne einen String gesendet wird.</response>
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
[HttpPut]
public async Task<IActionResult> Update([FromBody] UpdateEmailTemplateCommand update, CancellationToken cancel)
{
await mediator.Send(update, cancel);
return Ok();
}
}

View File

@@ -1,111 +0,0 @@
using EnvelopeGenerator.API.Extensions;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Envelopes.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
/// </summary>
/// <remarks>
/// Die API ermöglicht das Abrufen und Verwalten von Umschlägen basierend auf Benutzerinformationen und Statusfiltern.
///
/// Mögliche Antworten:
/// - 200 OK: Die Anfrage war erfolgreich, und die angeforderten Daten werden zurückgegeben.
/// - 400 Bad Request: Die Anfrage war fehlerhaft oder unvollständig.
/// - 401 Unauthorized: Der Benutzer ist nicht authentifiziert.
/// - 403 Forbidden: Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.
/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden.
/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten.
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class EnvelopeController : ControllerBase
{
private readonly ILogger<EnvelopeController> _logger;
private readonly IMediator _mediator;
/// <summary>
/// Erstellt eine neue Instanz des EnvelopeControllers.
/// </summary>
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
/// <param name="mediator"></param>
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
/// <summary>
/// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
/// </summary>
/// <param name="envelope"></param>
/// <returns>Eine IActionResult-Instanz, die die abgerufenen Umschläge oder einen Fehlerstatus enthält.</returns>
/// <response code="200">Die Anfrage war erfolgreich, und die Umschläge werden zurückgegeben.</response>
/// <response code="400">Die Anfrage war fehlerhaft oder unvollständig.</response>
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
[HttpGet]
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
{
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
return result.Any() ? Ok(result) : NotFound();
}
/// <summary>
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
/// </summary>
/// <param name="query"></param>
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[HttpGet("doc-result")]
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
{
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
var envelope = envelopes.FirstOrDefault();
if (envelope is null)
return NotFound("Envelope not available.");
if (envelope.DocResult is null)
return NotFound("The document has not been fully signed or the result has not yet been released.");
if (view)
{
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
return File(envelope.DocResult, "application/pdf");
}
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
}
/// <summary>
///
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
[NonAction]
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
{
var res = await _mediator.Send(command.WithAuth(User.GetId()));
if (res is null)
{
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
return StatusCode(StatusCodes.Status500InternalServerError);
}
else
return Ok(res);
}
}

View File

@@ -1,39 +0,0 @@
using MediatR;
using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
[Route("api/[controller]")]
[ApiController]
public class EnvelopeTypeController : ControllerBase
{
private readonly ILogger<EnvelopeTypeController> _logger;
private readonly IMediator _mediator;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="mediator"></param>
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetAllAsync()
{
var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
return Ok(result);
}
}

View File

@@ -1,38 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
namespace EnvelopeGenerator.API.Controllers.Interfaces;
/// <summary>
///
/// </summary>
public interface IAuthController
{
/// <summary>
///
/// </summary>
IAuthorizationService AuthService { get; }
/// <summary>
///
/// </summary>
ClaimsPrincipal User { get; }
}
/// <summary>
///
/// </summary>
public static class AuthControllerExtensions
{
/// <summary>
///
/// </summary>
/// <param name="controller"></param>
/// <param name="policyName"></param>
/// <returns></returns>
public static async Task<bool> IsUserInPolicyAsync(this IAuthController controller, string policyName)
{
var result = await controller.AuthService.AuthorizeAsync(controller.User, policyName);
return result.Succeeded;
}
}

View File

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

View File

@@ -1,47 +0,0 @@
using MediatR;
using EnvelopeGenerator.Application.Receivers.Queries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Controller für die Verwaltung von Empfängern.
/// </summary>
/// <remarks>
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ReceiverController : ControllerBase
{
private readonly IMediator _mediator;
/// <summary>
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
/// </summary>
/// <param name="mediator">Mediator für Anfragen.</param>
public ReceiverController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// Ruft eine Liste von Empfängern ab, basierend auf den angegebenen Abfrageparametern.
/// </summary>
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
[HttpGet]
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
{
if (!receiver.HasAnyCriteria)
{
var all = await _mediator.Send(new ReadReceiverQuery());
return Ok(all);
}
var result = await _mediator.Send(receiver);
return result is null ? NotFound() : Ok(result);
}
}

View File

@@ -1,57 +0,0 @@
using EnvelopeGenerator.API.Extensions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
///
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[ApiController]
[Route("api/[controller]")]
public class SignatureController : ControllerBase
{
private readonly IMediator _mediator;
/// <summary>
/// Initializes a new instance of <see cref="SignatureController"/>.
/// </summary>
public SignatureController(IMediator mediator)
{
_mediator = mediator;
}
//TODO: update to use signature query
/// <summary>
///
/// </summary>
/// <param name="envelopeKey"></param>
/// <param name="cancel"></param>
/// <returns></returns>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("{envelopeKey}")]
public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
{
int envelopeId = User.EnvelopeId();
int receiverId = User.ReceiverId();
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
return NotFound("Document is empty.");
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
if (rcvSignatures is null)
return NotFound("No signatures found for the current receiver.");
else
return Ok(rcvSignatures);
}
}

View File

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

View File

@@ -1,123 +0,0 @@
using EnvelopeGenerator.API.Models;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace EnvelopeGenerator.API.Documentation;
/// <summary>
///
/// </summary>
public sealed class AuthProxyDocumentFilter : IDocumentFilter
{
/// <summary>
///
/// </summary>
/// <param name="swaggerDoc"></param>
/// <param name="context"></param>
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
AddLoginOperation(swaggerDoc, context);
AddEnvelopeReceiverLoginOperation(swaggerDoc, context);
}
private static void AddLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
const string path = "/api/auth";
var loginSchema = context.SchemaGenerator.GenerateSchema(typeof(Login), context.SchemaRepository);
var loginExample = new OpenApiObject
{
["password"] = new OpenApiString(""),
["username"] = new OpenApiString("")
};
var operation = new OpenApiOperation
{
Summary = "Proxy login (auth-hub)",
Description = "Proxies the request to the auth service. Add query parameter `cookie=true|false`.",
Tags = [new() { Name = "Auth" }],
Parameters =
{
new OpenApiParameter
{
Name = "cookie",
In = ParameterLocation.Query,
Required = false,
Schema = new OpenApiSchema { Type = "boolean", Default = new OpenApiBoolean(true) },
Example = new OpenApiBoolean(true),
Description = "If true, auth service sets the auth cookie."
}
},
RequestBody = new OpenApiRequestBody
{
Required = true,
Content =
{
["application/json"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample },
["multipart/form-data"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample }
}
},
Responses =
{
["200"] = new OpenApiResponse { Description = "OK (proxied response)" },
["401"] = new OpenApiResponse { Description = "Unauthorized" }
}
};
swaggerDoc.Paths[path] = new OpenApiPathItem
{
Operations =
{
[OperationType.Post] = operation
}
};
}
private static void AddEnvelopeReceiverLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
const string path = "/api/Auth/envelope-receiver/{key}";
var bodySchema = context.SchemaGenerator.GenerateSchema(typeof(EnvelopeReceiverLogin), context.SchemaRepository);
var operation = new OpenApiOperation
{
Summary = "Envelope receiver login (auth-hub proxy)",
Description = "Proxies the envelope receiver login to the auth service. " +
"The `cookie` query parameter is always forwarded as `true` so the auth service sets the per-envelope cookie automatically.",
Tags = [new() { Name = "Auth" }],
Parameters =
{
new OpenApiParameter
{
Name = "key",
In = ParameterLocation.Path,
Required = true,
Schema = new OpenApiSchema { Type = "string" },
Description = "The unique envelope receiver key."
}
},
RequestBody = new OpenApiRequestBody
{
Required = false,
Content =
{
["multipart/form-data"] = new OpenApiMediaType { Schema = bodySchema }
}
},
Responses =
{
["200"] = new OpenApiResponse { Description = "OK per-envelope cookie set by auth service." },
["401"] = new OpenApiResponse { Description = "Unauthorized invalid or missing access code." }
}
};
swaggerDoc.Paths[path] = new OpenApiPathItem
{
Operations =
{
[OperationType.Post] = operation
}
};
}
}

View File

@@ -1,96 +0,0 @@
using DigitalData.Auth.Claims;
using Microsoft.IdentityModel.JsonWebTokens;
using System.Security.Claims;
namespace EnvelopeGenerator.API.Extensions;
/// <summary>
/// Provides helper methods for working with envelope-specific authentication claims.
/// </summary>
public static class ReceiverClaimExtensions
{
/// <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)
{
return value;
}
var identity = user.Identity;
var principalName = identity?.Name ?? "(anonymous)";
var authType = identity?.AuthenticationType ?? "(none)";
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
throw new InvalidOperationException(message);
}
private static string GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes)
{
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
{
var value = user.FindFirstValue(claimType);
if (!string.IsNullOrWhiteSpace(value))
return value;
}
var identity = user.Identity;
var principalName = identity?.Name ?? "(anonymous)";
var authType = identity?.AuthenticationType ?? "(none)";
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
var message = $"Required claim(s) '{string.Join("', '", claimTypes)}' are missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
throw new InvalidOperationException(message);
}
/// <summary>
/// Gets the authenticated envelope UUID from the claims.
/// </summary>
public static string EnvelopeUuid(this ClaimsPrincipal user)
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
/// <summary>
/// Gets the authenticated receiver signature from the claims.
/// </summary>
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 ReceiverMail(this ClaimsPrincipal user)
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
/// <summary>
/// Gets the authenticated envelope identifier from the claims.
/// </summary>
public static int EnvelopeId(this ClaimsPrincipal user)
{
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 ReceiverId(this ClaimsPrincipal user)
{
var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
if (int.TryParse(rcvIdStr, out var rcvId))
return rcvId;
else
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
}
}

View File

@@ -1,95 +0,0 @@
using System.Security.Claims;
namespace EnvelopeGenerator.API.Extensions
{
/// <summary>
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
/// </summary>
public static class SenderClaimExtensions
{
private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, string claimType)
{
var value = user.FindFirstValue(claimType);
if (value is not null)
{
return value;
}
var identity = user.Identity;
var principalName = identity?.Name ?? "(anonymous)";
var authType = identity?.AuthenticationType ?? "(none)";
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
throw new InvalidOperationException(message);
}
private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, params string[] claimTypes)
{
string? value = null;
foreach (var claimType in claimTypes)
{
value = user.FindFirstValue(claimType);
if (value is not null)
return value;
}
var identity = user.Identity;
var principalName = identity?.Name ?? "(anonymous)";
var authType = identity?.AuthenticationType ?? "(none)";
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
var message = $"Required claim among [{string.Join(", ", claimTypes)}] is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
throw new InvalidOperationException(message);
}
/// <summary>
/// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The user's ID as an integer.</returns>
/// <exception cref="InvalidOperationException">Thrown if the user ID claim is missing or invalid.</exception>
public static int GetId(this ClaimsPrincipal user)
{
var idValue = user.GetRequiredClaimOfSender(ClaimTypes.NameIdentifier, "sub");
if (!int.TryParse(idValue, out var result))
{
throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token.");
}
return result;
}
/// <summary>
/// Retrieves the username from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The username as a string.</returns>
public static string GetUsername(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.Name);
/// <summary>
/// Retrieves the user's surname (last name) from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The surname as a string.</returns>
public static string GetName(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.Surname);
/// <summary>
/// Retrieves the user's given name (first name) from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The given name as a string.</returns>
public static string GetPrename(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.GivenName);
/// <summary>
/// Retrieves the user's email address from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The email address as a string.</returns>
public static string GetEmail(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.Email);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
namespace EnvelopeGenerator.API.Models;
/// <summary>
/// Request body for the envelope-receiver login endpoint.
/// </summary>
/// <param name="AccessCode">The access code sent to the receiver.</param>
public record EnvelopeReceiverLogin(string? AccessCode = null);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ReverseProxy": {
"Clusters": {
"receiver-ui": {
"Destinations": {
"primary": {
"Address": "https://localhost:52936"
}
}
}
}
},
"AuthClientParams": {
"Url": "http://172.24.12.39:9090/auth-hub",
"PublicKeys": [
{
"Issuer": "auth.digitaldata.works",
"Audience": "sign-flow.digitaldata.works"
}
],
"RetryDelay": "00:00:05"
}
}

View File

@@ -1,185 +0,0 @@
{
"ReverseProxy": {
"Routes": {
"receiver-ui-root": {
"ClusterId": "receiver-ui",
"Order": 300,
"Match": {
"Path": "/",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-sender": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/sender/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-envelope": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/envelope/{EnvelopeKey}",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-envelope-dxreportviewer": {
"ClusterId": "receiver-ui",
"Order": 90,
"Match": {
"Path": "/envelope/{EnvelopeKey}/DxReportViewer",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-blazor-framework": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/_framework/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-blazor-content": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/_content/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-static-css": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/css/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{
"ResponseHeader": "Cache-Control",
"Set": "no-cache, no-store, must-revalidate",
"When": "Always"
}
]
},
"receiver-ui-static-js": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/js/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{
"ResponseHeader": "Cache-Control",
"Set": "no-cache, no-store, must-revalidate",
"When": "Always"
}
]
},
"receiver-ui-fake-data": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/fake-data/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-appsettings": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/appsettings.json",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-appsettings-dev": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/appsettings.Development.json",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-styles": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/EnvelopeGenerator.ReceiverUI.styles.css",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-fonts": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/fonts/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-images": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/images/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"auth-login": {
"ClusterId": "auth-hub",
"Match": {
"Path": "/api/auth",
"Methods": [ "POST" ]
},
"Transforms": [
{ "PathSet": "/api/auth/sign-flow" }
]
},
"auth-envelope-receiver-login": {
"ClusterId": "auth-hub",
"Match": {
"Path": "/api/Auth/envelope-receiver/{key}",
"Methods": [ "POST" ]
},
"Transforms": [
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
{
"QueryValueParameter": "cookie",
"Set": "true"
}
]
}
},
"Clusters": {
"receiver-ui": {
"Destinations": {
"primary": {
"Address": "https://localhost:52936"
}
}
},
"auth-hub": {
"Destinations": {
"primary": {
"Address": "http://172.24.12.39:9090"
}
}
}
}
}
}

View File

@@ -1,36 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Commands;
/// <summary>
///
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public class CreateCommandHandler<TCommand, TEntity> : IRequestHandler<TCommand, TEntity>
where TCommand : class, IRequest<TEntity>
where TEntity : class
{
/// <summary>
///
/// </summary>
protected readonly IRepository<TEntity> Repository;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public CreateCommandHandler(IRepository<TEntity> repository)
{
Repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task<TEntity> Handle(TCommand request, CancellationToken cancel) => Repository.CreateAsync(request, cancel);
}

View File

@@ -1,59 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using MediatR;
using System.Linq.Expressions;
namespace EnvelopeGenerator.Application.Common.Commands;
/// <summary>
///
/// </summary>
/// <typeparam name="TUpdateDto"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public abstract record UpdateCommand<TUpdateDto, TEntity> : IRequest where TUpdateDto : class where TEntity : class
{
/// <summary>
///
/// </summary>
public TUpdateDto Update { get; init; } = null!;
/// <summary>
///
/// </summary>
/// <returns></returns>
public abstract Expression<Func<TEntity, bool>> BuildQueryExpression();
}
/// <summary>
///
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <typeparam name="TUpdateDto"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public class UpdateCommandHandler<TCommand, TUpdateDto, TEntity> : IRequestHandler<TCommand>
where TUpdateDto : class
where TEntity : class
where TCommand : UpdateCommand<TUpdateDto, TEntity>
{
/// <summary>
///
/// </summary>
protected readonly IRepository<TEntity> Repository;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public UpdateCommandHandler(IRepository<TEntity> repository)
{
Repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Handle(TCommand request, CancellationToken cancel)
=> Repository.UpdateAsync(request.Update, request.BuildQueryExpression(), cancel);
}

View File

@@ -1,108 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public record AnnotationCreateDto
{
/// <summary>
///
/// </summary>
public long Id { get; set; }
/// <summary>
///
/// </summary>
[Obsolete("Not required for DevExpress")]
public int ElementId { get; init; } = -1;
/// <summary>
///
/// </summary>
[Obsolete("Not required for DevExpress")]
public string Name { get; init; } = string.Empty;
/// <summary>
///
/// </summary>
[Obsolete("Not required for DevExpress")]
public string Value { get; init; } = string.Empty;
/// <summary>
///
/// </summary>
[Obsolete("Not required for DevExpress")]
public string Type { get; init; } = string.Empty;
/// <summary>
/// Horizontal position of the signature field on the page.
/// <br/><br/>
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, X increases to the right.
/// <br/>
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
/// Convert: <c>xDX = xInches * 100.0</c>
/// <br/>
/// <b>Conversion to PDF Points:</b> Multiply by 72 (PSPDFKit, iText7 use 1/72 inch).
/// Convert: <c>xPt = xInches * 72.0</c>
/// </summary>
public double? X { get; init; }
/// <summary>
/// Vertical position of the signature field on the page.
/// <br/><br/>
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, Y increases downward.
/// <br/>
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
/// Convert: <c>yDX = yInches * 100.0</c>
/// <br/>
/// <b>Conversion to PDF Points (top-left origin):</b> Multiply by 72.
/// Convert: <c>yPt = yInches * 72.0</c>
/// <br/>
/// <b>Conversion to PDF Points (bottom-left origin - iText7):</b> Y-flip required.
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
/// </summary>
public double? Y { get; init; }
/// <summary>
///
/// </summary>
[Obsolete("Not required for DevExpress")]
public double? Width { get; init; }
/// <summary>
///
/// </summary>
[Obsolete("Not required for DevExpress")]
public double? Height { get; init; }
/// <summary>
/// Added to eliminate the need for SignatureDto in DevExpress
/// </summary>
public int? Page { get; init; }
}
/// <summary>
///
/// </summary>
public record AnnotationDto : AnnotationCreateDto
{
/// <summary>
///
/// </summary>
public long Id { get; init; }
/// <summary>
///
/// </summary>
public DateTime AddedWhen { get; init; }
/// <summary>
///
/// </summary>
public DateTime? ChangedWhen { get; init; }
/// <summary>
///
/// </summary>
public string? ChangedWho { get; init; }
}

View File

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

View File

@@ -1,65 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <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 Signature
{
/// <summary>
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
/// </summary>
public required int Id { 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; }
}

View File

@@ -1,27 +0,0 @@
namespace EnvelopeGenerator.Application.Common;
/// <summary>
///
/// </summary>
public enum EnvelopeFlag
{
/// <summary>
///
/// </summary>
EnvelopeOrReceiverNonexists,
/// <summary>
///
/// </summary>
NonDecodableEnvelopeReceiverId,
/// <summary>
///
/// </summary>
WrongEnvelopeReceiverId,
/// <summary>
///
/// </summary>
AccessCodeNull
}

View File

@@ -1,42 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Domain.Interfaces.Auditing;
using System;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
/// Extension methods for applying auditing timestamps during AutoMapper mappings.
/// </summary>
public static class AutoMapperAuditingExtensions
{
/// <summary>
/// Maps <see cref="IHasAddedWhen.AddedWhen"/> to the current UTC time.
/// </summary>
public static IMappingExpression<TSource, TDestination> MapAddedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
where TDestination : IHasAddedWhen
=> expression.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now));
/// <summary>
/// Maps <see cref="IHasChangedWhen.ChangedWhen"/> to the current UTC time.
/// </summary>
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
where TDestination : IHasChangedWhen
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
/// <summary>
/// Converts a base64 data URL string to a byte array.
/// Handles data URLs in the format: "data:image/png;base64,iVBORw0KG..."
/// </summary>
/// <param name="dataUrl">The base64 data URL string from Canvas.toDataURL()</param>
/// <returns>The decoded byte array, or null if the input is null or empty</returns>
public static byte[]? MapDataUrlToRequiredBytes(this string dataUrl)
{
// Remove data URL prefix (e.g., "data:image/png;base64,")
var base64Index = dataUrl.IndexOf(',', StringComparison.Ordinal);
if (base64Index == -1)
throw new ArgumentException("Invalid data URL format. Unable to extract base64 data.", nameof(dataUrl));
var base64Data = dataUrl[(base64Index + 1)..];
return Convert.FromBase64String(base64Data);
}
}

View File

@@ -1,182 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
using System.Text;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
///
/// </summary>
public static class DecodingExtensions
{
/// <summary>
/// Validates whether a given string is a correctly formatted Base-64 encoded string.
/// </summary>
/// <remarks>
/// This method checks the string for proper Base-64 formatting, which includes validating
/// the length of the string (must be divisible by 4). It also checks each character to ensure
/// it belongs to the Base-64 character set (A-Z, a-z, 0-9, '+', '/', and '=' for padding).
/// The method ensures that padding characters ('=') only appear at the end of the string and
/// are in a valid configuration (either one '=' at the end if the string's length % 4 is 3,
/// or two '==' if the length % 4 is 2).
/// </remarks>
/// <param name="input">The Base-64 encoded string to validate.</param>
/// <returns>
/// <c>true</c> if the string is a valid Base-64 encoded string; otherwise, <c>false</c>.
/// </returns>
/// <example>
/// <code>
/// string testString = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnk=";
/// bool isValid = IsValidBase64String(testString);
/// Console.WriteLine(isValid); // Output: true
/// </code>
/// </example>
public static bool IsBase64String(this string input)
{
// Check if the string is null or empty
if (string.IsNullOrEmpty(input))
{
return false;
}
// Replace valid base-64 padding
input = input.Trim();
int mod4 = input.Length % 4;
if (mod4 > 0)
{
// Base-64 string lengths should be divisible by 4
return false;
}
// Check each character to ensure it is valid base-64
foreach (char c in input)
{
if (!char.IsLetterOrDigit(c) && c != '+' && c != '/' && c != '=')
{
// Invalid character detected
return false;
}
}
// Ensure no invalid padding scenarios exist
if (input.EndsWith("==") && input.Length % 4 == 0 ||
input.EndsWith("=") && input.Length % 4 == 3)
{
return true;
}
return input.IndexOf('=') == -1; // No padding allowed except at the end
}
/// <summary>
///
/// </summary>
/// <param name="encodedKey"></param>
/// <param name="decodedKeys"></param>
/// <returns></returns>
public static bool TryDecode(this string encodedKey, out string[] decodedKeys)
{
try
{
byte[] bytes = Convert.FromBase64String(encodedKey);
string decodedString = Encoding.UTF8.GetString(bytes);
decodedKeys = decodedString.Split(new string[] { "::" }, StringSplitOptions.None);
return true;
}
catch(ArgumentNullException) { }
catch (FormatException) { }
catch(ArgumentException) { }
decodedKeys = Array.Empty<string>();
return false;
}
/// <summary>
///
/// </summary>
/// <param name="decodedKeys"></param>
/// <returns></returns>
public static EncodeType GetEncodeType(this string[] decodedKeys) => decodedKeys.Length switch
{
2 => EncodeType.EnvelopeReceiver,
3 => long.TryParse(decodedKeys[1], out var _) ? EncodeType.EnvelopeReceiverReadOnly : EncodeType.Undefined,
_ => EncodeType.Undefined,
};
/// <summary>
///
/// </summary>
/// <param name="decodedKeys"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public static (string? EnvelopeUuid, string? ReceiverSignature) ParseEnvelopeReceiverId(this string[] decodedKeys)
=> decodedKeys.GetEncodeType() == EncodeType.EnvelopeReceiver
? (EnvelopeUuid: decodedKeys[0], ReceiverSignature: decodedKeys[1])
: throw new InvalidOperationException("Attempted to convert a decoded other than type EnvelopeReceiver to EnvelopeReceiver.");
/// <summary>
///
/// </summary>
/// <param name="decodedKeys"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public static long ParseReadOnlyId(this string[] decodedKeys)
=> decodedKeys.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly
? long.Parse(decodedKeys[1])
: throw new InvalidOperationException("Attempted to convert a decoded other than type EnvelopeReceiver to EnvelopeReceiver. ");
/// <summary>
/// Decodes the envelope receiver ID and extracts the envelope UUID and receiver signature.
/// </summary>
/// <param name="envelopeReceiverId">The base64 encoded string containing the envelope UUID and receiver signature.</param>
/// <returns>A tuple containing the envelope UUID and receiver signature.</returns>
public static (string? EnvelopeUuid, string? ReceiverSignature) DecodeEnvelopeReceiverId(this string envelopeReceiverId)
{
if (!envelopeReceiverId.IsBase64String())
{
return (null, null);
}
byte[] bytes = Convert.FromBase64String(envelopeReceiverId);
string decodedString = Encoding.UTF8.GetString(bytes);
string[] parts = decodedString.Split(new string[] { "::" }, StringSplitOptions.None);
if (parts.Length > 1)
return (EnvelopeUuid: parts[0], ReceiverSignature: parts[1]);
else
return (string.Empty, string.Empty);
}
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiverReadOnlyId"></param>
/// <returns></returns>
public static long? DecodeEnvelopeReceiverReadOnlyId(this string envelopeReceiverReadOnlyId)
{
if (!envelopeReceiverReadOnlyId.IsBase64String())
{
return null;
}
byte[] bytes = Convert.FromBase64String(envelopeReceiverReadOnlyId);
string decodedString = Encoding.UTF8.GetString(bytes);
string[] parts = decodedString.Split(new string[] { "::" }, StringSplitOptions.None);
if (parts.Length > 2)
return long.TryParse(parts[1], out long readOnlyId) ? readOnlyId : null;
else
return null;
}
/// <summary>
/// Gets the envelope UUID from the decoded envelope receiver ID.
/// </summary>
/// <param name="envelopeReceiverId">The base64 encoded string to decode.</param>
/// <returns>The envelope UUID.</returns>
public static string? GetEnvelopeUuid(this string envelopeReceiverId) => envelopeReceiverId.DecodeEnvelopeReceiverId().EnvelopeUuid;
/// <summary>
/// Gets the receiver signature from the decoded envelope receiver ID.
/// </summary>
/// <param name="envelopeReceiverId">The base64 encoded string to decode.</param>
/// <returns>The receiver signature.</returns>
public static string? GetReceiverSignature(this string envelopeReceiverId) => envelopeReceiverId.DecodeEnvelopeReceiverId().ReceiverSignature;
}

View File

@@ -1,38 +0,0 @@
using System.Text;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
/// Provides extension methods for decoding and extracting information from an envelope receiver ID.
/// </summary>
public static class EncodingExtensions
{
/// <summary>
///
/// </summary>
/// <param name="readOnlyId"></param>
/// <returns></returns>
public static string ToEnvelopeKey(this long readOnlyId)
{
//The random number is used as a salt to increase security but it is not saved in the database.
string combinedString = $"{Random.Shared.Next()}::{readOnlyId}::{Random.Shared.Next()}";
byte[] bytes = Encoding.UTF8.GetBytes(combinedString);
string base64String = Convert.ToBase64String(bytes);
return base64String;
}
/// <summary>
///
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string ToEnvelopeKey(this (string envelopeUuid, string receiverSignature) input)
{
string combinedString = $"{input.envelopeUuid}::{input.receiverSignature}";
byte[] bytes = Encoding.UTF8.GetBytes(combinedString);
string base64String = Convert.ToBase64String(bytes);
return base64String;
}
}

View File

@@ -1,18 +0,0 @@
using System.Text.Json;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
///
/// </summary>
public static class JsonExtensions
{
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <param name="options"></param>
/// <returns></returns>
public static string ToJson(this object obj, JsonSerializerOptions? options = null)
=> JsonSerializer.Serialize(obj, options);
}

View File

@@ -1,125 +0,0 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
///
/// </summary>
public static class QueryExtensions
{
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="root"></param>
/// <param name="query"></param>
/// <param name="notnull"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public static IQueryable<TEntity> Where<TEntity>(this IQueryable<TEntity> root, EnvelopeQueryBase query, bool notnull = true)
where TEntity : IHasEnvelope
{
if (query.Id is not null)
root = root.Where(e => e.Envelope!.Id == query.Id);
else if (query.Uuid is not null)
root = root.Where(e => e.Envelope!.Uuid == query.Uuid);
else if (notnull)
throw new BadRequestException(
"Either Envelope Id or Envelope Uuid must be provided in the query."
);
return root;
}
/// <summary>
///
/// </summary>
/// <param name="root"></param>
/// <param name="query"></param>
/// <param name="notnull"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public static IQueryable<Envelope> Where(this IQueryable<Envelope> root, EnvelopeQueryBase query, bool notnull = true)
{
if (query.Id is not null)
root = root.Where(e => e.Id == query.Id);
else if (query.Uuid is not null)
root = root.Where(e => e.Uuid == query.Uuid);
else if (notnull)
throw new BadRequestException(
"Either Envelope Id or Envelope Uuid must be provided in the query."
);
return root;
}
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="root"></param>
/// <param name="query"></param>
/// <param name="notnull"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public static IQueryable<TEntity> Where<TEntity>(this IQueryable<TEntity> root, ReceiverQueryBase query, bool notnull = true)
where TEntity : IHasReceiver
{
if (query.Id is not null)
root = root.Where(e => e.Receiver!.Id == query.Id);
else if (query.EmailAddress is not null)
root = root.Where(e => e.Receiver!.EmailAddress == query.EmailAddress);
else if (query.Signature is not null)
root = root.Where(e => e.Receiver!.Signature == query.Signature);
else if (notnull)
throw new BadRequestException(
"Receiver must have at least one identifier (Id, EmailAddress, or Signature)."
);
return root;
}
/// <summary>
///
/// </summary>
/// <param name="root"></param>
/// <param name="query"></param>
/// <param name="notnull"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public static IQueryable<Receiver> Where(this IQueryable<Receiver> root, ReceiverQueryBase query, bool notnull = true)
{
if (query.Id is not null)
root = root.Where(e => e.Id == query.Id);
else if (query.EmailAddress is not null)
root = root.Where(e => e.EmailAddress == query.EmailAddress);
else if (query.Signature is not null)
root = root.Where(e => e.Signature == query.Signature);
else if (notnull)
throw new BadRequestException(
"Receiver must have at least one identifier (Id, EmailAddress, or Signature)."
);
return root;
}
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TEnvelopeQuery"></typeparam>
/// <typeparam name="TReceiverQuery"></typeparam>
/// <param name="root"></param>
/// <param name="query"></param>
/// <param name="notnull"></param>
/// <returns></returns>
public static IQueryable<TEntity> Where<TEntity, TEnvelopeQuery, TReceiverQuery>(this IQueryable<TEntity> root, EnvelopeReceiverQueryBase<TEnvelopeQuery, TReceiverQuery> query, bool notnull = true)
where TEntity : IHasEnvelope, IHasReceiver
where TEnvelopeQuery : EnvelopeQueryBase, new()
where TReceiverQuery : ReceiverQueryBase, new()
=> root.Where(query.Envelope, notnull).Where(query.Receiver, notnull);
}

View File

@@ -1,30 +0,0 @@
using OtpNet;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
///
/// </summary>
public static class StringExtension
{
/// <summary>
///
/// </summary>
/// <param name="totp"></param>
/// <param name="secret"></param>
/// <returns></returns>
public static bool IsValidTotp(this string totp, string secret)
{
var secret_bytes = Base32Encoding.ToBytes(secret);
var secret_totp = new Totp(secret_bytes);
return secret_totp.VerifyTotp(totp, out _, VerificationWindow.RfcSpecifiedNetworkDelay);
}
/// <summary>
///
/// </summary>
/// <param name="seperator"></param>
/// <param name="values"></param>
/// <returns></returns>
public static string Join(this IEnumerable<string> values, string seperator) => string.Join(seperator, values);
}

View File

@@ -1,105 +0,0 @@
using DigitalData.Core.Exceptions;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
/// Extension methods for tasks
/// </summary>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static class TaskExtensions
{
/// <summary>
/// Awaits the specified task and ensures that the result is not <c>null</c>.
/// If the result is <c>null</c>, the exception created by factory-method is thrown.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <typeparam name="TException">The type of the exception.</typeparam>
/// <param name="task">The task to await.</param>
/// <param name="factory">Exception provider</param>
/// <returns>The awaited result if not <c>null</c>.</returns>
/// <exception>Thrown if the result is <c>null</c>.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception
{
var result = await task;
return result ?? throw factory();
}
/// <summary>
/// Awaits the specified task and ensures that the result is not <c>empty</c>.
/// If the result contains no elements, the exception created by factory-method is thrown.
/// </summary>
/// <typeparam name="T">The element type of the collection.</typeparam>
/// <typeparam name="TException">The type of the exception.</typeparam>
/// <param name="task">The task to await.</param>
/// <param name="factory">Exception provider</param>
/// <returns>The awaited collection if it is not <c>null</c> or empty.</returns>
/// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception
{
var result = await task;
return result?.Any() ?? false ? result : throw factory();
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="I"></typeparam>
/// <param name="task"></param>
/// <param name="act"></param>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act)
{
var res = await task;
return act(res);
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="task"></param>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task) => task.Then(t => t.FirstOrDefault());
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TException"></typeparam>
/// <param name="task"></param>
/// <param name="factory"></param>
/// <returns></returns>
public static Task<T> FirstAsync<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory)
where TException : Exception
=> task.Then(t => t.FirstOrDefault() ?? throw factory());
}
/// <summary>
///
/// </summary>
public static class Exceptions
{
/// <summary>
///
/// </summary>
public static NotFoundException NotFound() => new();
/// <summary>
///
/// </summary>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static BadRequestException BadRequest() => new();
/// <summary>
///
/// </summary>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static ForbiddenException Forbidden() => new();
}

View File

@@ -1,43 +0,0 @@
using Ganss.Xss;
using Microsoft.Extensions.Localization;
using System.Text.Encodings.Web;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
///
/// </summary>
public static class XSSExtensions
{
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="encoder"></param>
/// <returns></returns>
public static string? TryEncode(this string? value, UrlEncoder encoder) => value is null ? value : encoder.Encode(value);
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="encoder"></param>
/// <returns></returns>
public static string? TryEncode(this LocalizedString? value, UrlEncoder encoder) => value is null ? null : encoder.Encode(value);
/// <summary>
///
/// </summary>
/// <param name="html"></param>
/// <param name="sanitizer"></param>
/// <returns></returns>
public static string? TrySanitize(this string? html, HtmlSanitizer sanitizer) => html is null ? html : sanitizer.Sanitize(html);
/// <summary>
///
/// </summary>
/// <param name="html"></param>
/// <param name="sanitizer"></param>
/// <returns></returns>
public static string? TrySanitize(this LocalizedString? html, HtmlSanitizer sanitizer) => html is null ? null : sanitizer.Sanitize(html);
}

View File

@@ -1,13 +0,0 @@
using DigitalData.Core.Abstraction.Application;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// <summary>
///
/// </summary>
[Obsolete("Use MediatR")]
public interface IEnvelopeDocumentService : IBasicCRUDService<DocumentDto, Document, int>
{
}

View File

@@ -1,37 +0,0 @@
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
/// <summary>
/// Notification raised when a document is signed by a receiver.
/// </summary>
[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 => EnvelopeReceiver.Receiver?.EmailAddress
?? throw new InvalidOperationException($"Receiver is null." +
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
}

View File

@@ -1,39 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Application.Common.Dto;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// <summary>
///
/// </summary>
[Obsolete("The PSPDFKit library is deprecated.")]
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
{
/// <summary>
///
/// </summary>
private readonly IRepository<ElementAnnotation> _repo;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public AnnotationHandler(IRepository<ElementAnnotation> repository)
{
_repo = repository;
}
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
{
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
}
}

View File

@@ -1,43 +0,0 @@
using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using System.Text.Json;
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 = "{}";
private readonly ISender _sender;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public DocStatusHandler(ISender sender)
{
_sender = sender;
}
/// <summary>
///
/// </summary>
/// <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.EnvelopeReceiver.EnvelopeId,
ReceiverId = notification.EnvelopeReceiver.ReceiverId,
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson
}, cancel);
}

View File

@@ -1,42 +0,0 @@
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// <summary>
///
/// </summary>
public class HistoryHandler : INotificationHandler<DocSignedNotification>
{
private readonly ISender _sender;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public HistoryHandler(ISender sender)
{
_sender = sender;
}
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
{
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.EnvelopeReceiver.EnvelopeId,
UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress,
Status = EnvelopeStatus.DocumentSigned,
}, cancel);
}
}

View File

@@ -1,70 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// <summary>
///
/// </summary>
public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
{
/// <summary>
///
/// </summary>
/// <param name="tempRepo"></param>
/// <param name="emailOutRepo"></param>
/// <param name="mailParamsOptions"></param>
/// <param name="dispatcherParamsOptions"></param>
public SendSignedMailHandler(IRepository<EmailTemplate> tempRepo, IRepository<EmailOut> emailOutRepo, IOptions<MailParams> mailParamsOptions, IOptions<DispatcherParams> dispatcherParamsOptions) : base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
{
}
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="emailOut"></param>
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
{
emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
}
/// <summary>
/// </summary>
/// <param name="notification"></param>
/// <returns></returns>
protected override Dictionary<string, string> CreatePlaceHolders(DocSignedNotification notification)
{
var placeHolders = new Dictionary<string, string>()
{
{ "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? string.Empty },
{ "[DOCUMENT_TITLE]", notification.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
};
if (notification.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;
}
}

View File

@@ -1,53 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
/// <summary>
///
/// </summary>
public class RemoveAnnotationHandler : INotificationHandler<RemoveSignatureNotification>
{
private readonly IRepository<ElementAnnotation> _repo;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public RemoveAnnotationHandler(IRepository<ElementAnnotation> repository)
{
_repo = repository;
}
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
{
notification.ThrowIfHasNoFilter();
return _repo.DeleteAsync(annots =>
{
// envelope ID filter
if (notification.EnvelopeId is int envelopeId)
annots = annots.Where(annot => annot.Element!.Document.EnvelopeId == envelopeId);
// envelope UUID filter
if (notification.EnvelopeUuid is string envelopeUuid)
annots = annots.Where(annot => annot.Element!.Document.Envelope!.Uuid == envelopeUuid);
// receiver ID
if (notification.ReceiverId is int receiverId)
annots = annots.Where(annot => annot.Element!.ReceiverId == receiverId);
// receiver signature
if (notification.ReceiverSignature is string receiverSignature)
annots = annots.Where(annot => annot.Element!.Receiver!.Signature == receiverSignature);
return annots;
}, cancel);
}
}

View File

@@ -1,53 +0,0 @@
using AngleSharp.Html;
using DigitalData.Core.Abstraction.Application.Repository;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
/// <summary>
///
/// </summary>
public class RemoveDocStatusHandler : INotificationHandler<RemoveSignatureNotification>
{
private readonly IRepository<Domain.Entities.DocumentStatus> _repo;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public RemoveDocStatusHandler(IRepository<Domain.Entities.DocumentStatus> repository)
{
_repo = repository;
}
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
{
notification.ThrowIfHasNoFilter();
return _repo.DeleteAsync(statuses =>
{
// envelope ID filter
if (notification.EnvelopeId is int envelopeId)
statuses = statuses.Where(status => status.EnvelopeId == envelopeId);
// envelope UUID filter
if (notification.EnvelopeUuid is string envelopeUuid)
statuses = statuses.Where(status => status.Envelope!.Uuid == envelopeUuid);
// receiver Id filter
if (notification.ReceiverId is int receiverId)
statuses = statuses.Where(status => status.ReceiverId == receiverId);
// receiver signature filter
if (notification.ReceiverSignature is string receiverSignature)
statuses = statuses.Where(status => status.Receiver!.Signature == receiverSignature);
return statuses;
}, cancel);
}
}

View File

@@ -1,56 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
/// <summary>
///
/// </summary>
public class RemoveHistoryHandler : INotificationHandler<RemoveSignatureNotification>
{
private readonly IRepository<Domain.Entities.History> _repo;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public RemoveHistoryHandler(IRepository<Domain.Entities.History> repository)
{
_repo = repository;
}
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
{
notification.ThrowIfHasNoFilter();
return _repo.DeleteAsync(hists =>
{
hists = hists.Where(hist => hist.Status == EnvelopeStatus.DocumentSigned);
// envelope ID filter
if (notification.EnvelopeId is int envelopeId)
hists = hists.Where(hist => hist.EnvelopeId == envelopeId);
// envelope UUID filter
if (notification.EnvelopeUuid is string envelopeUuid)
hists = hists.Where(hist => hist.Envelope!.Uuid == envelopeUuid);
// receiver ID filter
if (notification.ReceiverId is int receiverId)
hists = hists.Where(hist => hist.Receiver!.Id == receiverId);
// receiver signature filter
if (notification.ReceiverSignature is string receiverSignature)
hists = hists.Where(hist => hist.Receiver!.Signature == receiverSignature);
return hists;
}, cancel);
}
}

View File

@@ -1,37 +0,0 @@
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
/// <summary>
///
/// </summary>
/// <param name="EnvelopeId"></param>
/// <param name="ReceiverId"></param>
/// <param name="EnvelopeUuid"></param>
/// <param name="ReceiverSignature"></param>
public record RemoveSignatureNotification(
int? EnvelopeId = null,
int? ReceiverId = null,
string? EnvelopeUuid = null,
string? ReceiverSignature = null
) : INotification
{
/// <summary>
///
/// </summary>
public bool HasFilter =>
EnvelopeId is not null
|| ReceiverId is not null
|| EnvelopeUuid is not null
|| ReceiverSignature is not null;
/// <summary>
///
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public void ThrowIfHasNoFilter()
{
if (!HasFilter)
throw new InvalidOperationException("At least one filter parameter must be provided.");
}
}

View File

@@ -1,135 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace EnvelopeGenerator.Application.Common.Notifications;
/// <summary>
///
/// </summary>
public interface ISendMailNotification : INotification
{
/// <summary>
///
/// </summary>
public EmailTemplateType TemplateType { get; }
/// <summary>
///
/// </summary>
public string EmailAddress { get; }
}
/// <summary>
///
/// </summary>
public abstract class SendMailHandler<TNotification> : INotificationHandler<TNotification>
where TNotification : ISendMailNotification
{
/// <summary>
///
/// </summary>
protected readonly IRepository<EmailTemplate> TempRepo;
/// <summary>
///
/// </summary>
protected readonly IRepository<EmailOut> EmailOutRepo;
/// <summary>
///
/// </summary>
protected abstract Dictionary<string, string> CreatePlaceHolders(TNotification notification);
/// <summary>
///{ "[MESSAGE]", notification.Message },<br/>
///{ "[DOCUMENT_ACCESS_CODE]", notification.ReceiverAccessCode },<br/>
///{ "[REASON]", pReason }<br/>
///{ "[NAME_SENDER]", notification.Envelope.User?.FullName},<br/>
///{ "[NAME_PORTAL]", DispatcherParams. },<br/>
///{ "[SIGNATURE_TYPE]", "signieren" },<br/>
///{ "[LINK_TO_DOCUMENT]", notification.SignatureLink },<br/>
///{ "[LINK_TO_DOCUMENT_TEXT]", $"{notification.SignatureLink.Truncate(40)}.." },
/// </summary>
protected readonly MailParams MailParams;
/// <summary>
///
/// </summary>
protected readonly DispatcherParams DispatcherParams;
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="emailOut"></param>
protected abstract void ConfigureEmailOut(TNotification notification, EmailOut emailOut);
/// <summary>
///
/// </summary>
/// <param name="tempRepo"></param>
/// <param name="emailOutRepo"></param>
/// <param name="mailParamsOptions"></param>
/// <param name="dispatcherParamsOptions"></param>
protected SendMailHandler(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="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
{
var placeHolders = CreatePlaceHolders(notification);
var temp = await TempRepo
.Where(x => x.Name == notification.TemplateType.ToString())
.SingleOrDefaultAsync(cancel)
?? throw new InvalidOperationException($"Receiver information is missing in the notification." +
$"{typeof(TNotification)}:\n {notification.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 = notification.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,
};
ConfigureEmailOut(notification, emailOut);
await EmailOutRepo.CreateAsync(emailOut, cancel);
}
private static string ReplacePlaceHolders(string text, params Dictionary<string, string>[] placeHoldersList)
{
foreach (var placeHolders in placeHoldersList)
foreach (var ph in placeHolders)
text = text.Replace(ph.Key, ph.Value);
return text;
}
}

View File

@@ -1,17 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Query;
/// <summary>
/// Repräsentiert eine Abfrage für Umschläge.
/// </summary>
public record EnvelopeQueryBase
{
/// <summary>
/// Die eindeutige Kennung des Umschlags.
/// </summary>
public virtual int? Id { get; set; }
/// <summary>
/// Die universell eindeutige Kennung des Umschlags.
/// </summary>
public virtual string? Uuid { get; set; }
}

View File

@@ -1,62 +0,0 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Application.Common.Query;
/// <summary>
///
/// </summary>
public record EnvelopeReceiverQueryBase : EnvelopeReceiverQueryBase<EnvelopeQueryBase, ReceiverQueryBase>;
/// <summary>
///
/// </summary>
/// <typeparam name="TEnvelopeQuery"></typeparam>
/// <typeparam name="TReceiverQuery"></typeparam>
public record EnvelopeReceiverQueryBase<TEnvelopeQuery, TReceiverQuery>
where TEnvelopeQuery : EnvelopeQueryBase, new()
where TReceiverQuery : ReceiverQueryBase, new()
{
private string? _key;
/// <summary>
///
/// </summary>
public virtual string? Key
{
get => _key;
set
{
if (value is null)
{
_key = null;
return;
}
(string? EnvelopeUuid, string? ReceiverSignature) = value.DecodeEnvelopeReceiverId();
if (string.IsNullOrEmpty(EnvelopeUuid) || string.IsNullOrEmpty(ReceiverSignature))
throw new BadRequestException("Der EnvelopeReceiverKey muss ein gültiger Base64-kodierter String sein, der die EnvelopeUuid und die ReceiverSignature enthält.");
Envelope = new TEnvelopeQuery()
{
Uuid = EnvelopeUuid
};
Receiver = new TReceiverQuery()
{
Signature = ReceiverSignature
};
_key = value;
}
}
/// <summary>
/// Repräsentiert eine Abfrage für Umschläge.
/// </summary>
public virtual TEnvelopeQuery Envelope { get; set; } = new();
/// <summary>
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
/// um spezifische Informationen über einen Empfänger abzurufen.
/// </summary>
public virtual TReceiverQuery Receiver { get; set; } = new();
}

View File

@@ -1,20 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Query;
/// <summary>
///
/// </summary>
public class MappingProfile : Profile
{
/// <summary>
///
/// </summary>
public MappingProfile()
{
CreateMap<EnvelopeQueryBase, Envelope>();
CreateMap<ReceiverQueryBase, Receiver>();
CreateMap<EnvelopeReceiverQueryBase, EnvelopeReceiver>();
}
}

View File

@@ -1,33 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Query;
/// <summary>
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
/// um spezifische Informationen über einen Empfänger abzurufen.
/// </summary>
public record ReceiverQueryBase
{
/// <summary>
/// ID des Empfängers
/// </summary>
public virtual int? Id { get; set; }
/// <summary>
/// E-Mail Adresse des Empfängers
/// </summary>
public virtual string? EmailAddress { get; set; }
/// <summary>
/// Eindeutige Signatur des Empfängers
/// </summary>
public virtual string? Signature { get; set; }
/// <summary>
/// Checks whether any of the specified query criteria have a value.
/// </summary>
/// <remarks>
/// This property returns <c>true</c> if at least one of the fields
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
/// </remarks>
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
}

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Application.Common.Configurations;
namespace EnvelopeGenerator.Application.Configurations;
/// <summary>
///

View File

@@ -0,0 +1,8 @@
namespace EnvelopeGenerator.Application.Configurations;
/// <summary>
///
/// </summary>
public class DbTriggerParams : Dictionary<string, ICollection<string>>
{
}

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Application.Common.Configurations;
namespace EnvelopeGenerator.Application.Configurations;
/// <summary>
///
@@ -23,5 +23,5 @@ public class DispatcherParams
/// <summary>
/// Default value is string.Empty
/// </summary>
public string? EmailAttmt1 { get; init; } = null;
public string EmailAttmt1 { get; init; } = string.Empty;
}

View File

@@ -1,5 +1,5 @@
using DigitalData.Core.Client.Interface;
namespace EnvelopeGenerator.Application.Common.Configurations;
namespace EnvelopeGenerator.Application.Configurations;
/// <summary>
/// https://www.gtx-messaging.com/en/api-docs/sms-rest-api/

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Application.Common.Configurations;
namespace EnvelopeGenerator.Application.Configurations;
/// <summary>
///

View File

@@ -1,7 +1,7 @@
using OtpNet;
using System.Globalization;
namespace EnvelopeGenerator.Application.Common.Configurations;
namespace EnvelopeGenerator.Application.Configurations;
/// <summary>
///

View File

@@ -1,7 +1,7 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///

View File

@@ -1,11 +1,11 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///
/// </summary>
[Obsolete("Use IRepository")]
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int>
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocumentReceiverElement, int>
{
}

View File

@@ -1,7 +1,7 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///

View File

@@ -1,8 +1,8 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using static EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///

View File

@@ -0,0 +1,12 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///
/// </summary>
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
public interface IEnvelopeCertificateRepository : ICRUDRepository<EnvelopeCertificate, int>
{
}

View File

@@ -1,12 +1,12 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///
/// </summary>
[Obsolete("Use IRepository")]
public interface IEnvelopeDocumentRepository : ICRUDRepository<Document, int>
public interface IEnvelopeDocumentRepository : ICRUDRepository<EnvelopeDocument, int>
{
}

View File

@@ -1,14 +1,14 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///
/// </summary>
[Obsolete("Use IRepository")]
public interface IEnvelopeHistoryRepository : ICRUDRepository<History, long>
public interface IEnvelopeHistoryRepository : ICRUDRepository<EnvelopeHistory, long>
{
/// <summary>
///
@@ -17,7 +17,7 @@ public interface IEnvelopeHistoryRepository : ICRUDRepository<History, long>
/// <param name="userReference"></param>
/// <param name="status"></param>
/// <returns></returns>
Task<int> CountAsync(int? envelopeId = null, string? userReference = null, EnvelopeStatus? status = null);
Task<int> CountAsync(int? envelopeId = null, string? userReference = null, Constants.EnvelopeStatus? status = null);
/// <summary>
///
@@ -28,5 +28,5 @@ public interface IEnvelopeHistoryRepository : ICRUDRepository<History, long>
/// <param name="withSender"></param>
/// <param name="withReceiver"></param>
/// <returns></returns>
Task<IEnumerable<History>> ReadAsync(int? envelopeId = null, string? userReference = null, EnvelopeStatus? status = null, bool withSender = false, bool withReceiver = false);
Task<IEnumerable<EnvelopeHistory>> ReadAsync(int? envelopeId = null, string? userReference = null, Constants.EnvelopeStatus? status = null, bool withSender = false, bool withReceiver = false);
}

View File

@@ -1,7 +1,7 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///

View File

@@ -1,9 +1,7 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Envelopes.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///
@@ -85,7 +83,7 @@ public interface IEnvelopeReceiverRepository : ICRUDRepository<EnvelopeReceiver,
/// <param name="max_status"></param>
/// <param name="ignore_statuses"></param>
/// <returns></returns>
Task<IEnumerable<EnvelopeReceiver>> ReadByUsernameAsync(string username, EnvelopeStatus? min_status = null, EnvelopeStatus? max_status = null, params EnvelopeStatus[] ignore_statuses);
Task<IEnumerable<EnvelopeReceiver>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses);
/// <summary>
///

View File

@@ -1,8 +1,7 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///
@@ -39,5 +38,5 @@ public interface IEnvelopeRepository : ICRUDRepository<Envelope, int>
/// <param name="max_status"></param>
/// <param name="ignore_statuses"></param>
/// <returns></returns>
Task<IEnumerable<Envelope>> ReadByUserAsync(int userId, EnvelopeStatus? min_status = null, EnvelopeStatus? max_status = null, params EnvelopeStatus[] ignore_statuses);
Task<IEnumerable<Envelope>> ReadByUserAsync(int userId, int? min_status = null, int? max_status = null, params int[] ignore_statuses);
}

View File

@@ -1,7 +1,7 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///

View File

@@ -1,7 +1,7 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
namespace EnvelopeGenerator.Application.Contracts.Repositories;
/// <summary>
///

View File

@@ -1,6 +1,6 @@
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///
@@ -14,5 +14,5 @@ public interface IDocumentExecutor
/// <param name="envelope_uuid"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
Task<Document> CreateDocumentAsync(string base64, string envelope_uuid, CancellationToken cancellation = default);
Task<EnvelopeDocument> CreateDocumentAsync(string base64, string envelope_uuid, CancellationToken cancellation = default);
}

View File

@@ -1,7 +1,7 @@
using Dapper;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///

View File

@@ -1,6 +1,6 @@
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
/// Provides methods for executing common queries on a given entity type.

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
/// Represents a raw SQL query contract.

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
/// Defines methods for executing raw SQL queries or custom SQL query classes and returning query executors for further operations.

View File

@@ -1,6 +1,6 @@
using Dapper;
namespace EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///

View File

@@ -1,6 +1,6 @@
using OtpNet;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///

View File

@@ -1,9 +1,9 @@
using DigitalData.Core.Abstraction.Application;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///

View File

@@ -1,13 +1,13 @@
using DigitalData.Core.Abstraction.Application;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///
/// </summary>
[Obsolete("Use MediatR")]
public interface IDocumentReceiverElementService : IBasicCRUDService<DocReceiverElementDto, DocReceiverElement, int>
public interface IDocumentReceiverElementService : IBasicCRUDService<DocumentReceiverElementDto, DocumentReceiverElement, int>
{
}

View File

@@ -1,7 +1,7 @@
using DigitalData.Core.Abstraction.Application;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///

View File

@@ -1,10 +1,10 @@
using DigitalData.Core.Abstraction.Application;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities;
using static EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///

View File

@@ -0,0 +1,13 @@
using DigitalData.Core.Abstraction.Application;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///
/// </summary>
[Obsolete("Use MediatR")]
public interface IEnvelopeCertificateService : IBasicCRUDService<EnvelopeCertificateDto, EnvelopeCertificate, int>
{
}

View File

@@ -0,0 +1,13 @@
using DigitalData.Core.Abstraction.Application;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///
/// </summary>
[Obsolete("Use MediatR")]
public interface IEnvelopeDocumentService : IBasicCRUDService<EnvelopeDocumentDto, EnvelopeDocument, int>
{
}

View File

@@ -1,17 +1,17 @@
using DigitalData.Core.Abstraction.Application;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.DTOs.EnvelopeHistory;
using EnvelopeGenerator.Application.DTOs.Receiver;
using EnvelopeGenerator.Domain.Entities;
using static EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///
/// </summary>
[Obsolete("Use MediatR")]
public interface IEnvelopeHistoryService : ICRUDService<HistoryCreateDto, HistoryDto, History, long>
public interface IEnvelopeHistoryService : ICRUDService<EnvelopeHistoryCreateDto, EnvelopeHistoryDto, EnvelopeHistory, long>
{
/// <summary>
///
@@ -56,7 +56,7 @@ public interface IEnvelopeHistoryService : ICRUDService<HistoryCreateDto, Histor
/// <param name="withSender"></param>
/// <param name="withReceiver"></param>
/// <returns></returns>
Task<IEnumerable<HistoryDto>> ReadAsync(int? envelopeId = null, string? userReference = null, ReferenceType? referenceType = null, EnvelopeStatus? status = null, bool withSender = false, bool withReceiver = false);
Task<IEnumerable<EnvelopeHistoryDto>> ReadAsync(int? envelopeId = null, string? userReference = null, ReferenceType? referenceType = null, EnvelopeStatus? status = null, bool withSender = false, bool withReceiver = false);
/// <summary>
///
@@ -64,14 +64,14 @@ public interface IEnvelopeHistoryService : ICRUDService<HistoryCreateDto, Histor
/// <param name="envelopeId"></param>
/// <param name="userReference"></param>
/// <returns></returns>
Task<IEnumerable<HistoryDto>> ReadRejectedAsync(int envelopeId, string? userReference = null);
Task<IEnumerable<EnvelopeHistoryDto>> ReadRejectedAsync(int envelopeId, string? userReference = null);
/// <summary>
///
/// </summary>
/// <param name="envelopeId"></param>
/// <returns></returns>
Task<IEnumerable<ReceiverDto>> ReadRejectingReceivers(int envelopeId);
Task<IEnumerable<ReceiverReadDto>> ReadRejectingReceivers(int envelopeId);
/// <summary>
///

View File

@@ -1,10 +1,10 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.EmailProfilerDispatcher.Abstraction.Contracts;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Domain;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Application.Contracts.Services;
/// <summary>
///
@@ -19,7 +19,7 @@ public interface IEnvelopeMailService : IEmailOutService
/// <param name="tempType"></param>
/// <param name="optionalPlaceholders"></param>
/// <returns></returns>
Task<DataResult<int>> SendAsync(EnvelopeReceiverDto envelopeReceiverDto, EmailTemplateType tempType, Dictionary<string, object>? optionalPlaceholders = null);
Task<DataResult<int>> SendAsync(EnvelopeReceiverDto envelopeReceiverDto, Constants.EmailTemplateType tempType, Dictionary<string, object>? optionalPlaceholders = null);
/// <summary>
///
@@ -35,4 +35,11 @@ public interface IEnvelopeMailService : IEmailOutService
/// <param name="envelopeReceiverDto"></param>
/// <returns></returns>
Task<DataResult<int>> SendAccessCodeAsync(EnvelopeReceiverDto envelopeReceiverDto);
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiverDto"></param>
/// <returns></returns>
Task<DataResult<int>> SendTFAQrCodeAsync(EnvelopeReceiverDto envelopeReceiverDto);
}

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