Compare commits
325 Commits
120485ee8d
...
feat/remov
| Author | SHA1 | Date | |
|---|---|---|---|
| b46c3c8ebe | |||
| 9f712a6b8c | |||
| 4449743105 | |||
| 435be882c9 | |||
| 09257d7766 | |||
| 16fa4b460f | |||
| f4aae8c787 | |||
| ed1468194d | |||
| f9f94c900e | |||
| 18df6221c9 | |||
| b15f95755c | |||
| 4be9e38a9e | |||
| 11c8886d6f | |||
| 1ad673904a | |||
| 32a9d008a4 | |||
| 454dd0b6cc | |||
| aa2a31e993 | |||
| ecc509919c | |||
| f3751274b1 | |||
| adc231f10e | |||
| 999fac7dac | |||
| 86758bb127 | |||
| 498d59a4ce | |||
| ce430c1b80 | |||
| eb219291de | |||
| 80733ab1f7 | |||
| 3d9153da23 | |||
| a0bfd405f0 | |||
| ec3a15a467 | |||
| 80f08b393c | |||
| b22d57d7da | |||
| 01cd8b2607 | |||
| 310d2efc6e | |||
| e5402bafc9 | |||
| f517ab4cfb | |||
| 6d63f9db2b | |||
| 46c7c31e03 | |||
| 8f6db435bb | |||
| 9bf7676464 | |||
| d9556c18ca | |||
| 5b7157f422 | |||
| 119ee3ec16 | |||
| b536b31a4b | |||
| a80efee075 | |||
| 86d6daafbd | |||
| aefa1a8883 | |||
| b3e7f77165 | |||
| bac9bfd036 | |||
| 6effb8a0d6 | |||
| a9ff0d88f2 | |||
| 0fdfe991ea | |||
| 7fc2c14f9c | |||
| 5098deedd1 | |||
| 2e634361c5 | |||
| 60ebebc0d8 | |||
| c2d8b6531c | |||
| 665266457f | |||
| 14d581acac | |||
| eb533a1dc2 | |||
| 36025fad7b | |||
| 0606efabf9 | |||
| 2cb1e266f7 | |||
| 35c1374b9c | |||
| feaea1626b | |||
| babd96200a | |||
| f9cfcf1f19 | |||
| ffb9e07466 | |||
| 5384a08905 | |||
| dda5ff28e1 | |||
| 043349a47f | |||
| c36f7ccf98 | |||
| cf3e85bcd7 | |||
| 77379265c2 | |||
| 9b68c09b0b | |||
| cd6bf05352 | |||
| b2c205b160 | |||
| 33439f4a65 | |||
| b5cdf79799 | |||
| 72915ac001 | |||
| e4d7a0d8f3 | |||
| d5f9de06f3 | |||
| bbc129178f | |||
| 40c95100ff | |||
| 1b2731b4b2 | |||
| ae56902758 | |||
| e80059d34e | |||
| a2443032c5 | |||
| 3d2fe532e5 | |||
| 5e83aa26c9 | |||
| 2c789cd4c0 | |||
| 9ecfe08e2e | |||
| 74da6e37b0 | |||
| 10f65e583a | |||
| 0cdc8f1191 | |||
| 56ccab6377 | |||
| 4d069cdaa0 | |||
| bbbfa4de01 | |||
| 3ff3373b27 | |||
| 2af8815cf6 | |||
| ca24a96084 | |||
| db368b889a | |||
| 8f451b9c2c | |||
| 6120e6062e | |||
| cc2aea90ed | |||
| 762a9e8bca | |||
| 6ed4caea4f | |||
| d94821433a | |||
| 278b9964f1 | |||
| e6722803bb | |||
| 47bc7675c9 | |||
| 789e312316 | |||
| 2a9bbb3fe5 | |||
| bc34317720 | |||
| 76ff3e47e1 | |||
| 2d22bfcd06 | |||
| 185c783824 | |||
| b957b4b4bb | |||
| df154d83cc | |||
| 49ec9fbead | |||
| 01fc29f59e | |||
| 733b70cca2 | |||
| 8f4b751303 | |||
| a5e4f97397 | |||
| 6ca03a50eb | |||
| 96a84ba1a5 | |||
| ec0ea72890 | |||
| 7b912387e7 | |||
| 6c142eba08 | |||
| 489d2808a1 | |||
| 7466fd78f6 | |||
| e34b5ddbbe | |||
| b56f906848 | |||
| fe09c5c7ae | |||
| 0763d82f6e | |||
| 5a30bc050b | |||
| a4b218b9f3 | |||
| 67798b35da | |||
| b5bb2bbaae | |||
| 85a0736106 | |||
| de9c9da176 | |||
| f4571320ce | |||
| 2abfffdeba | |||
| bfd1a9d060 | |||
| 78ed49a077 | |||
| 6aa97adf84 | |||
| 7456babe0d | |||
| 71e375d6ea | |||
| 05f64e2b61 | |||
| ed17852542 | |||
| 9947774ba8 | |||
| c6c1decd2a | |||
| 0fdaa1a38d | |||
| 5d66de9f32 | |||
| b6ec5307b6 | |||
| 106e62a912 | |||
| 27940f5d34 | |||
| e776c2edb4 | |||
| 3f0f5d7fb9 | |||
| e11bc9df8e | |||
| 4dca17d39c | |||
| 8baf6b5553 | |||
| 3ca99fdd83 | |||
| 9e37bf1fe2 | |||
| 9a0837caa9 | |||
| 030646f33d | |||
| 88317e40f5 | |||
| 6fe99d0cd0 | |||
| 45018d04b1 | |||
| b5af3e61ed | |||
| 314608f27f | |||
| ba9f233993 | |||
| 9d962708c4 | |||
| c93a056ca5 | |||
| a88a26c248 | |||
| 1e963ea215 | |||
| 02b857382c | |||
| ca4ec7cb6f | |||
| f2356b3ce4 | |||
| d61fe79613 | |||
| 714cb9555f | |||
| 315a022cb8 | |||
| 746635979b | |||
| 31548728cd | |||
| 06c8af2ed8 | |||
| 9f57baf2e5 | |||
| 73d793f0a0 | |||
| 65bb68feef | |||
| c5e97ee30b | |||
| 3a4f449b59 | |||
| 6ca7767e4d | |||
| 4237f0a815 | |||
| 3302be9348 | |||
| 4572e20c51 | |||
| b3a70d7259 | |||
| bb81920d44 | |||
| 3b66de0691 | |||
| 9f6004ba8c | |||
| ef246bae32 | |||
| e4ebb29969 | |||
| 83cdb9dfe9 | |||
| c5db676e01 | |||
| 95c8e15887 | |||
| 561b844e59 | |||
| 011960be75 | |||
| 3a2fa77862 | |||
| cfa6dbd2de | |||
| eb2603f389 | |||
| 456178bee1 | |||
| 2c41c74510 | |||
| bb73795d68 | |||
| 207992d95a | |||
| d6bafc64a6 | |||
| 3090711892 | |||
| 9dbd8f7952 | |||
| 48a41f2987 | |||
| 96688a951c | |||
| 6f07de3ec4 | |||
| 4611266224 | |||
| c529d03129 | |||
| 829fab9647 | |||
| b2e3605b54 | |||
| 8cbdee2491 | |||
| 151c785af9 | |||
| fa354a05cc | |||
| 1326407462 | |||
| a3c653ddb3 | |||
| 8d736cdc5e | |||
| 4281eaeb22 | |||
| 150fca5f47 | |||
| 1f889d8b58 | |||
| d599fe3156 | |||
| 6c40c48ac8 | |||
| 536b8ef5da | |||
| d35a35c75e | |||
| 7fb1a87cf2 | |||
| a3b33637fd | |||
| bc3134a033 | |||
| f106255c6b | |||
| cb103dcb69 | |||
| 8c1dd9c40d | |||
| ee358ffaab | |||
| 0780dbdd94 | |||
| d722742fe8 | |||
| 8c42105f58 | |||
| 88b196ed6d | |||
| c99511de29 | |||
| 7d0c5a0ee5 | |||
| 7001d7351f | |||
| cf16312394 | |||
| ecb7f45f14 | |||
| 0ba5578d94 | |||
| e093471a24 | |||
| b16ae70762 | |||
| c3e8f09291 | |||
| a9fb82bbea | |||
| 895fd5c509 | |||
| 3b4278d7e0 | |||
| 6d1fb05e10 | |||
| 26da78fa22 | |||
| 9eee2b523d | |||
| 9dc2b9adef | |||
| cc3c5ec9f0 | |||
| 2766d963af | |||
| 8fd9928524 | |||
| fb3ee14f8f | |||
| e3929a99e3 | |||
| b6d86aa3eb | |||
| 4171a3138b | |||
| e98e18cfe0 | |||
| 14aff03de4 | |||
| d828a5bfe2 | |||
| a6e174e7c1 | |||
| fc7aa83513 | |||
| 90661cb856 | |||
| 04e30b0d79 | |||
| d0a50f63db | |||
| 9d20ba1987 | |||
| 66f7b6f5e1 | |||
| a49dd0ff81 | |||
| e420e8d47a | |||
| d7f86adffe | |||
| cfb5c15fda | |||
| a0ed3e2fe4 | |||
| f5505190e9 | |||
| 1bfdbac8ff | |||
| 4a29511491 | |||
| 1089304bf1 | |||
| 9b606a0d3b | |||
| cb6dea319b | |||
| d59aa6157d | |||
| 1569647b60 | |||
| 3bb2a013ab | |||
| 215b755f92 | |||
| 3a94733047 | |||
| 7793d3cbb9 | |||
| 9174065365 | |||
| 19824afc1c | |||
| c7fe3f0b9c | |||
| a98024063a | |||
| e0cab3f965 | |||
| e5347b063d | |||
| ecd695ad37 | |||
| 5a8809ffc1 | |||
| b832637a6a | |||
| fceb266630 | |||
| 87bdef9d5e | |||
| 2fb32fb982 | |||
| 63d050244c | |||
| 126a4acb12 | |||
| 082cb322ef | |||
| cd077aa1bd | |||
| 49ac35153e | |||
| 91a563d995 | |||
| 308cdd03f2 | |||
| f35068e368 | |||
| e490805025 | |||
| 08550f87e6 | |||
| a22ec7a7d3 | |||
| f4681f85e7 | |||
| 0ed4a44df0 | |||
| b8926f25c4 | |||
| 5b220932d3 | |||
| 50c02314ef | |||
| 223bb88f54 | |||
| 38f4da00da |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -365,3 +365,8 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
||||||
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
||||||
/EnvelopeGenerator.Tests.Application/annotations.json
|
/EnvelopeGenerator.Tests.Application/annotations.json
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/ignore/EnvelopeGenerator.Form
|
||||||
|
|||||||
324
AGENTS.md
Normal file
324
AGENTS.md
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# EnvelopeGenerator - Agent Guide
|
||||||
|
|
||||||
|
## Must Read First
|
||||||
|
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
||||||
|
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Catalog
|
||||||
|
|
||||||
|
This section explains every markdown file in the repo: what it contains, why it exists, and when you need it. Read this before deciding which file to open.
|
||||||
|
|
||||||
|
### Core Architecture Docs (Active — Always Relevant)
|
||||||
|
|
||||||
|
#### `AGENTS.md` ← you are here
|
||||||
|
**What:** Authoritative agent/copilot onboarding guide. Covers project structure, route table, render mode rules, coordinate system, API gaps, and common mistakes.
|
||||||
|
**Why it exists:** Single source of truth for agents so they don't re-derive architecture from code on every session.
|
||||||
|
**When you need it:** Always. Read first on every session.
|
||||||
|
**Maintenance:** Update whenever project structure, routes, render modes, or naming conventions change.
|
||||||
|
|
||||||
|
#### `COPILOT_CONTEXT.md`
|
||||||
|
**What:** Deep-dive architecture reference. Covers the full Blazor Auto hybrid setup, authentication flows (JWT + cookies, per-envelope receiver tokens), service registration patterns, coordinate system proof, and an explicit "prefer Server/Server.Client over old WebUI/ReceiverUI" rule.
|
||||||
|
**Why it exists:** Prevents agents from re-deriving or misinterpreting the hybrid SSR+WASM architecture. Also documents coordinate system evidence to prevent regressions (inches vs. points vs. pixels).
|
||||||
|
**When you need it:**
|
||||||
|
- Implementing any new page or service
|
||||||
|
- Debugging auth/cookie issues
|
||||||
|
- Any coordinate conversion question
|
||||||
|
- Unsure which project a new file should go in
|
||||||
|
**Maintenance:** Update when render mode assignments change, new auth schemes are added, or the coordinate system is touched.
|
||||||
|
|
||||||
|
#### `FORM_APPLICATION_CONTEXT.md`
|
||||||
|
**What:** Comprehensive reverse-engineering of the legacy VB.NET WinForms app (`EnvelopeGenerator.Form`). Documents every form (frmMain, frmEnvelopeEditor, frmFieldEditor, etc.), their UI layout, toolbar actions, data models, coordinate system, and a feature-by-feature migration mapping table to `EnvelopeGenerator.Server`.
|
||||||
|
**Why it exists:** The WinForms app is the functional specification for the sender-side web UI. It documents the full intended workflow that is being migrated to Blazor — especially the envelope creation, signature field placement, and dashboard features that are not yet implemented.
|
||||||
|
**When you need it:**
|
||||||
|
- Implementing any sender-side feature (`/sender`, `/sender/editor`, `/sender/envelope/{id}`)
|
||||||
|
- Understanding what a specific toolbar button or form should do
|
||||||
|
- Designing API endpoints for sender workflow
|
||||||
|
- Checking coordinate system behavior (INCHES proof is here)
|
||||||
|
**Maintenance:** Update the mapping table as features are implemented (change `❌ Not implemented` → `✅ Exists`). Do not rename `ReceiverUI` back — it was the old project name; current target is `EnvelopeGenerator.Server`.
|
||||||
|
|
||||||
|
#### `RECEIVER_PDF_VIEWER_CONTEXT.md`
|
||||||
|
**What:** Detailed technical reference for the receiver-side PDF viewer (`EnvelopeReceiverPage.razor`). Documents the PDF.js integration, signature button rendering, JS interop API (`window.pdfViewer`), signature capture flow, HiDPI/zoom/thumbnail config, and the full component lifecycle.
|
||||||
|
**Why it exists:** The receiver PDF viewer is the most complex component in the codebase. This doc prevents agents from misunderstanding the JS↔Blazor boundary, re-implementing already-solved problems (HiDPI, zoom, signature caching), or breaking the signing flow.
|
||||||
|
**When you need it:**
|
||||||
|
- Working on `EnvelopeReceiverPage.razor` or `pdf-viewer.js`
|
||||||
|
- Adding new signature field rendering logic
|
||||||
|
- Debugging signature rendering, zoom, or thumbnail issues
|
||||||
|
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
|
||||||
|
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Deployment Docs
|
||||||
|
|
||||||
|
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
||||||
|
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
|
||||||
|
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
|
||||||
|
**When you need it:**
|
||||||
|
- Deploying to IIS for the first time
|
||||||
|
- Troubleshooting 500 errors on production after deploy
|
||||||
|
- Setting up a new deployment environment
|
||||||
|
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Active Architecture (Post-Migration)
|
||||||
|
|
||||||
|
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||||
|
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||||
|
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||||
|
|
||||||
|
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0)
|
||||||
|
|
||||||
|
**Proxy:** YARP in EnvelopeGenerator.Server routes `/api/*` → `localhost:8088` (API)
|
||||||
|
|
||||||
|
### Deprecated Projects - DO NOT USE
|
||||||
|
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
||||||
|
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
|
||||||
|
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Run Both Projects (Required)
|
||||||
|
```powershell
|
||||||
|
# Terminal 1 - API Backend
|
||||||
|
cd EnvelopeGenerator.API
|
||||||
|
dotnet run
|
||||||
|
|
||||||
|
# Terminal 2 - Blazor Frontend
|
||||||
|
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical:** Both must run simultaneously. EnvelopeGenerator.Server proxy forwards `/api/*` to API.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```powershell
|
||||||
|
dotnet build EnvelopeGenerator.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Boundaries
|
||||||
|
|
||||||
|
```
|
||||||
|
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
|
||||||
|
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
||||||
|
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
||||||
|
EnvelopeGenerator.API/ # Controllers, endpoints
|
||||||
|
EnvelopeGenerator.Server/
|
||||||
|
EnvelopeGenerator.Server/ # Server-side Blazor components
|
||||||
|
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||||
|
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
||||||
|
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||||
|
├─ Services/ # HTTP API clients
|
||||||
|
├─ Models/ # DTOs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Structure (Critical)
|
||||||
|
|
||||||
|
| Route | File Location | Render Mode | Purpose |
|
||||||
|
|-------|--------------|-------------|---------|
|
||||||
|
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
|
||||||
|
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
||||||
|
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
|
||||||
|
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
|
||||||
|
| `/envelope/login/{key}` | `Server.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
||||||
|
| `/envelope/{key}` | `Server/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
|
||||||
|
|
||||||
|
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
|
||||||
|
|
||||||
|
## Coordinate System (CRITICAL)
|
||||||
|
|
||||||
|
**Database stores INCHES** (GdPicture14 native). Origin: top-left, Y-axis down.
|
||||||
|
|
||||||
|
### Conversions
|
||||||
|
```csharp
|
||||||
|
// Database (INCHES) → PDF Points
|
||||||
|
float points = inches * 72;
|
||||||
|
|
||||||
|
// Database (INCHES) → DevExpress DX
|
||||||
|
float dx = inches * 100;
|
||||||
|
|
||||||
|
// PDF.js Pixels → Database (INCHES)
|
||||||
|
float inches = (pixelX / canvasWidth) * pageWidthInches;
|
||||||
|
```
|
||||||
|
|
||||||
|
**A4 Page:** 8.27" wide × 11.69" tall = 595pt × 842pt
|
||||||
|
|
||||||
|
**Signature Field Size:** 1.77" × 1.96" (FIXED, do not change)
|
||||||
|
|
||||||
|
**Evidence:** See `COPILOT_CONTEXT.md` lines 158-185, `EnvelopeGenerator.Form/frmFieldEditor.vb`
|
||||||
|
|
||||||
|
## API Architecture Quirks
|
||||||
|
|
||||||
|
### Monolithic Endpoint (Avoid for UI)
|
||||||
|
`POST /api/EnvelopeReceiver` - Creates envelope+document+receivers+fields atomically.
|
||||||
|
- **Use case:** External API consumers
|
||||||
|
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
|
||||||
|
|
||||||
|
### Missing Granular Endpoints (Need to Create)
|
||||||
|
```
|
||||||
|
POST /api/Envelope/draft # Create draft envelope
|
||||||
|
PUT /api/Envelope/{id} # Update metadata
|
||||||
|
DELETE /api/Envelope/{id} # Delete with reason
|
||||||
|
POST /api/Envelope/{id}/document # Upload PDF
|
||||||
|
POST /api/Envelope/{id}/receivers # Add receiver
|
||||||
|
POST /api/Envelope/{id}/signature-fields # Place signature field
|
||||||
|
POST /api/Envelope/{id}/send # Send to receivers
|
||||||
|
```
|
||||||
|
|
||||||
|
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
||||||
|
|
||||||
|
## Status Color Coding
|
||||||
|
|
||||||
|
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
||||||
|
.envelope-row.status-queued,
|
||||||
|
.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */
|
||||||
|
.envelope-row.status-completed { background-color: #81C784; }
|
||||||
|
.envelope-row.status-deleted,
|
||||||
|
.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
|
||||||
|
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
||||||
|
|
||||||
|
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"PdfViewerOptions": {
|
||||||
|
"ThumbnailBaseScale": 0.75,
|
||||||
|
"ThumbnailEnableHiDPI": true,
|
||||||
|
"MainCanvasEnableHiDPI": true,
|
||||||
|
"ZoomStepPercentage": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Config (`API/appsettings.json`)
|
||||||
|
- `ConnectionStrings:Default` - SQL Server DB
|
||||||
|
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
||||||
|
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
||||||
|
- `PSPDFKitLicenseKey` - **DEPRECATED** (use PDF.js instead)
|
||||||
|
|
||||||
|
## Migration Status
|
||||||
|
|
||||||
|
### Complete ✅
|
||||||
|
- Receiver login/authentication
|
||||||
|
- PDF viewing with PDF.js (HiDPI, zoom, thumbnails)
|
||||||
|
- Signature capture (draw/type/image)
|
||||||
|
- Signature caching (Redis/SQL)
|
||||||
|
- Sender login
|
||||||
|
|
||||||
|
### Missing (High Priority) ❌
|
||||||
|
- Sender dashboard (`/sender`) - Empty stub
|
||||||
|
- Envelope editor (`/sender/envelope/{id}`)
|
||||||
|
- Signature field placement tool (PDF.js + draggable overlays)
|
||||||
|
- Granular API endpoints (draft, receivers, fields)
|
||||||
|
- Master-detail grids for receivers/history
|
||||||
|
|
||||||
|
## Common Mistakes (DO NOT REPEAT)
|
||||||
|
|
||||||
|
| Mistake | Why Wrong |
|
||||||
|
|---------|-----------|
|
||||||
|
| Using iText7 in receiver pages | GPL license issue. Use PDF.js overlays. |
|
||||||
|
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
||||||
|
| `@rendermode InteractiveWebAssembly` on PDF viewers | DevExpress DxPdfViewer requires server-side rendering. |
|
||||||
|
| Hardcoded quality in PDF.js | Use `appsettings.json` `PdfViewerOptions`. |
|
||||||
|
| Coordinates in points/pixels for DB | Database uses INCHES. Convert before save. |
|
||||||
|
| `BottomMarginBand` for signatures | Repeats on every page. Use `DetailBand`. |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**No automated tests exist yet.**
|
||||||
|
|
||||||
|
Manual testing workflow:
|
||||||
|
1. Start API (`dotnet run` in `EnvelopeGenerator.API`)
|
||||||
|
2. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||||
|
3. Navigate to `https://localhost:5131` (or check console output for port)
|
||||||
|
4. Test sender login at `/sender/login`
|
||||||
|
5. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
**SQL Server** (DD_ECM)
|
||||||
|
- Connection string in `API/appsettings.json`
|
||||||
|
- EF Core migrations NOT used (manual SQL scripts)
|
||||||
|
- Stored procedures: `PRSIG_*` prefix
|
||||||
|
|
||||||
|
**Key Tables:**
|
||||||
|
- `TBSIG_ENVELOPE` - Envelope metadata
|
||||||
|
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
|
||||||
|
- `TBSIG_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
|
||||||
|
- `TBSIG_RECEIVER` - Receiver registry
|
||||||
|
- `TBSIG_DOCUMENT` - PDF binary data
|
||||||
|
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
||||||
|
|
||||||
|
## DevExpress
|
||||||
|
|
||||||
|
**License:** Commercial (v25.2.3)
|
||||||
|
**Components Used:**
|
||||||
|
- `DxGrid` - Master-detail grids
|
||||||
|
- `DxPdfViewer` - Server-side PDF rendering
|
||||||
|
- `DxPopup` - Modal dialogs
|
||||||
|
- `DxToolbar` - Action bars
|
||||||
|
- `DxFormLayout` - Forms
|
||||||
|
|
||||||
|
**Theme:** Blazing Berry (default)
|
||||||
|
|
||||||
|
## JavaScript Interop
|
||||||
|
|
||||||
|
**PDF Viewer:** `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 Pad:** `wwwroot/js/receiver-signature.js`
|
||||||
|
```javascript
|
||||||
|
window.receiverSignature = {
|
||||||
|
initializeDrawPad(canvasId, dotNetRef),
|
||||||
|
getSignatureDataUrl(canvasId),
|
||||||
|
clearPad(canvasId)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Envelope Support
|
||||||
|
|
||||||
|
Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies:
|
||||||
|
```
|
||||||
|
AuthTokenSignFLOWReceiver.{envelopeKey}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each envelope maintains independent authentication state.
|
||||||
|
|
||||||
|
## External Dependencies
|
||||||
|
|
||||||
|
**CDN:**
|
||||||
|
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
|
|
||||||
|
**NuGet (Server.Client):**
|
||||||
|
- `DevExpress.Blazor.*` 25.2.3
|
||||||
|
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
||||||
|
|
||||||
|
**External Services:**
|
||||||
|
- LDAP/AD authentication (optional)
|
||||||
|
- GTX Messaging (SMS 2FA)
|
||||||
|
- Email dispatcher (signFlow)
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
None required. All config in `appsettings.json`.
|
||||||
|
|
||||||
|
**Local dev ports:**
|
||||||
|
- API: `https://localhost:8088`
|
||||||
|
- WebUI: `https://localhost:5131` or `http://localhost:7192`
|
||||||
351
COPILOT_CONTEXT.md
Normal file
351
COPILOT_CONTEXT.md
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# EnvelopeGenerator — Current Workspace Context
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Digital document signing system for senders and receivers.
|
||||||
|
|
||||||
|
- Senders authenticate, view envelope lists, and manage envelope workflows.
|
||||||
|
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
|
||||||
|
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
|
||||||
|
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Active Application Structure
|
||||||
|
|
||||||
|
### Main Host
|
||||||
|
**Primary active application:** `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
|
`EnvelopeGenerator.Server` is the current runtime host and contains:
|
||||||
|
- Blazor server host
|
||||||
|
- WebAssembly host integration
|
||||||
|
- API controllers
|
||||||
|
- authentication/authorization setup
|
||||||
|
- Swagger/Scalar setup
|
||||||
|
- YARP reverse proxy configuration
|
||||||
|
- DevExpress server-side services
|
||||||
|
- SQL Server distributed cache setup
|
||||||
|
|
||||||
|
### Client Project
|
||||||
|
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||||
|
|
||||||
|
This project contains:
|
||||||
|
- WebAssembly-rendered pages
|
||||||
|
- client-side services
|
||||||
|
- client models and options
|
||||||
|
- sender and receiver login flows
|
||||||
|
|
||||||
|
### Other Projects
|
||||||
|
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
|
||||||
|
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||||
|
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||||
|
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||||
|
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
|
### Legacy / Do Not Touch
|
||||||
|
- `EnvelopeGenerator.Service`
|
||||||
|
- `EnvelopeGenerator.Form`
|
||||||
|
- `EnvelopeGenerator.BBTests`
|
||||||
|
- `EnvelopeGenerator.CommonServices`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Hosting Model
|
||||||
|
|
||||||
|
`EnvelopeGenerator.Server/Program.cs` currently configures:
|
||||||
|
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
|
||||||
|
- `AddControllers()` and `MapControllers()`
|
||||||
|
- JWT authentication for sender and receiver flows
|
||||||
|
- cookie authentication
|
||||||
|
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
|
||||||
|
- `AddReverseProxy()` with `yarp.json`
|
||||||
|
- Swagger / OpenAPI / Scalar
|
||||||
|
- distributed SQL Server cache
|
||||||
|
- DevExpress Blazor and DevExpress PDF Viewer server-side services
|
||||||
|
- request localization middleware
|
||||||
|
|
||||||
|
This means the active app is a **merged UI + API host**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reverse Proxy
|
||||||
|
|
||||||
|
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
|
|
||||||
|
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
|
||||||
|
|
||||||
|
Configured routes forward:
|
||||||
|
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
|
||||||
|
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Active Routes and Files
|
||||||
|
|
||||||
|
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||||
|
| Route | File | Render Mode | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
|
||||||
|
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
|
||||||
|
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
|
||||||
|
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
|
||||||
|
|
||||||
|
### Server Pages (`EnvelopeGenerator.Server`)
|
||||||
|
| Route | File | Render Mode | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
|
||||||
|
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
|
||||||
|
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
|
||||||
|
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current API Location
|
||||||
|
|
||||||
|
The active application exposes controllers from:
|
||||||
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||||
|
|
||||||
|
Current controller set includes:
|
||||||
|
- `AnnotationController`
|
||||||
|
- `AuthController`
|
||||||
|
- `CacheController`
|
||||||
|
- `ConfigController`
|
||||||
|
- `DocumentController`
|
||||||
|
- `EmailTemplateController`
|
||||||
|
- `EnvelopeController`
|
||||||
|
- `EnvelopeReceiverController`
|
||||||
|
- `EnvelopeTypeController`
|
||||||
|
- `HistoryController`
|
||||||
|
- `LocalizationController`
|
||||||
|
- `ReadOnlyController`
|
||||||
|
- `ReceiverController`
|
||||||
|
- `SignatureController`
|
||||||
|
- `TfaRegistrationController`
|
||||||
|
|
||||||
|
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication Model
|
||||||
|
|
||||||
|
### Sender
|
||||||
|
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||||
|
|
||||||
|
Key sender endpoints:
|
||||||
|
- `POST /api/auth?cookie=true` — login
|
||||||
|
- `GET /api/auth/check` — current sender access check
|
||||||
|
- `POST /api/auth/logout` — logout
|
||||||
|
|
||||||
|
### Receiver
|
||||||
|
Receiver authentication is **per envelope**.
|
||||||
|
|
||||||
|
Key receiver endpoints used by client services:
|
||||||
|
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
|
||||||
|
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
|
||||||
|
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
|
||||||
|
|
||||||
|
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||||
|
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||||
|
|
||||||
|
### Receiver Server-Side Authorization
|
||||||
|
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||||
|
|
||||||
|
It uses:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- tries the current `HttpContext.User`
|
||||||
|
- if needed, reads the per-envelope receiver cookie directly
|
||||||
|
- validates the JWT with the receiver auth scheme
|
||||||
|
- verifies the token subject matches the route envelope key
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Receiver Page Data Loading
|
||||||
|
|
||||||
|
Main server-side page data service:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|
|
||||||
|
This service loads directly via MediatR and distributed cache:
|
||||||
|
- document bytes
|
||||||
|
- receiver envelope data
|
||||||
|
- signature placeholders
|
||||||
|
- cached signature data
|
||||||
|
|
||||||
|
For signature placeholders, the service:
|
||||||
|
- reads document receiver elements
|
||||||
|
- filters them for the authenticated receiver
|
||||||
|
- converts coordinates to `UnitOfLength.Point` before UI use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Receiver PDF Viewer
|
||||||
|
|
||||||
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||||
|
|
||||||
|
Current receiver viewer characteristics:
|
||||||
|
- route: `/envelope/{EnvelopeKey}`
|
||||||
|
- render mode: `InteractiveServer`
|
||||||
|
- PDF rendering: `PDF.js`
|
||||||
|
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
|
||||||
|
- signature popup: `DxPopup`
|
||||||
|
- thumbnail sidebar: resizable and stored in `localStorage`
|
||||||
|
|
||||||
|
### JS Assets
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
|
||||||
|
|
||||||
|
### PDF.js CDN
|
||||||
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signature Workflow
|
||||||
|
|
||||||
|
Receiver signatures are handled as a **viewer overlay workflow**.
|
||||||
|
|
||||||
|
### Current behavior
|
||||||
|
1. Server-side authorization validates receiver access.
|
||||||
|
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
|
||||||
|
3. If no cached signature exists, the signature popup opens automatically.
|
||||||
|
4. Receiver creates signature using one of three tabs:
|
||||||
|
- draw
|
||||||
|
- text
|
||||||
|
- image
|
||||||
|
5. Required metadata:
|
||||||
|
- full name
|
||||||
|
- place
|
||||||
|
6. Optional metadata:
|
||||||
|
- position
|
||||||
|
7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer.
|
||||||
|
|
||||||
|
### Important note
|
||||||
|
Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer.
|
||||||
|
|
||||||
|
### Signature DTO
|
||||||
|
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record SignatureCaptureDto {
|
||||||
|
public required string DataUrl { get; init; }
|
||||||
|
public required string FullName { get; init; }
|
||||||
|
public string Position { get; init; } = "";
|
||||||
|
public required string Place { get; init; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signature Cache
|
||||||
|
|
||||||
|
### Active cache model
|
||||||
|
The current receiver page cache flow is handled directly in the server project through:
|
||||||
|
- `EnvelopeReceiverPageDataService`
|
||||||
|
- `IDistributedCache`
|
||||||
|
- SQL Server distributed cache configuration from `Program.cs`
|
||||||
|
|
||||||
|
### Cache key format
|
||||||
|
Current server-side key prefix:
|
||||||
|
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||||
|
|
||||||
|
This is different from an envelope-key-only cache convention.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||||
|
- section name: `Cache`
|
||||||
|
- option: `SignatureCacheExpiration`
|
||||||
|
|
||||||
|
### Related controller
|
||||||
|
A cache API controller also exists in:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sender Dashboard
|
||||||
|
|
||||||
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
- checks sender access through `AuthService.CheckSenderAccessAsync()`
|
||||||
|
- redirects to `/sender/login` when unauthorized
|
||||||
|
- loads envelope list through client `EnvelopeService`
|
||||||
|
- separates envelopes into active/completed tabs
|
||||||
|
- uses `DevExpress DxGrid`
|
||||||
|
|
||||||
|
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
Current server host localization setup in `Program.cs`:
|
||||||
|
- supported cultures: `de-DE`, `en-US`
|
||||||
|
- request localization middleware is enabled
|
||||||
|
- `QueryStringRequestCultureProvider` is added
|
||||||
|
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||||
|
|
||||||
|
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
### Source data
|
||||||
|
Database signature coordinates are still based on:
|
||||||
|
- **unit:** inches
|
||||||
|
- **origin:** top-left
|
||||||
|
- **axes:** X right, Y down
|
||||||
|
|
||||||
|
### Relevant conversions
|
||||||
|
- inches -> PDF points: `x_pt = x_inches * 72`
|
||||||
|
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
|
||||||
|
|
||||||
|
### Current receiver page behavior
|
||||||
|
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
|
||||||
|
|
||||||
|
### Unit systems to keep in mind
|
||||||
|
| System | Unit | Origin | Y-axis |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Database | Inches | Top-left | Down |
|
||||||
|
| PDF.js display | Pixels | Top-left | Down |
|
||||||
|
| PDF points | Points | Depends on PDF model | Depends on consumer |
|
||||||
|
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Services and Files
|
||||||
|
|
||||||
|
### Client services
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||||
|
|
||||||
|
### Server services
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|
|
||||||
|
### Server config and host files
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Working Rules for This Workspace
|
||||||
|
|
||||||
|
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||||
|
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||||
|
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||||
|
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
|
||||||
|
- Do not modify the legacy VB.NET projects unless explicitly requested.
|
||||||
|
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
|
||||||
|
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2026-06-29
|
||||||
@@ -1,946 +0,0 @@
|
|||||||
# EnvelopeGenerator — Copilot Context Notes (English)
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
A digital document signing system. Senders upload PDFs and place signature annotation fields via PSPDFKit (EnvelopeGenerator.Web). Receivers open the document in a Blazor WASM viewer, confirm each signature field via a checkbox overlay, draw/type/upload their signature, and export the stamped PDF.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Solution Structure
|
|
||||||
|
|
||||||
| Project | Target | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Receiver auth (cookie), annotation reading, PDF serving. |
|
|
||||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | Blazor WebAssembly. Receiver UI. YARP proxies API calls. |
|
|
||||||
| `EnvelopeGenerator.Web` | net7/8/9 | Razor Pages. Sender UI + PSPDFKit annotation placement. |
|
|
||||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. |
|
|
||||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
|
||||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
|
||||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities. NOT used in ReceiverUI flow. |
|
|
||||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
|
||||||
| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `ReceiverUI/Pages/EnvelopeViewer.razor` | **NEW** PDF.js-based viewer (`/envelope/{key}`). Replaces ReportViewer.razor. Simple read-only PDF viewing with zoom/navigation. |
|
|
||||||
| `ReceiverUI/Pages/ReportViewer.razor` | **LEGACY** DevExpress-based signing page (`/receiver/{key}`). Still used for signature workflow. Will be deprecated. |
|
|
||||||
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | **NEW** PDF.js wrapper: rendering, zoom, pagination, mouse wheel control. |
|
|
||||||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | JS: checkbox overlay, signature pad (draw/type/image). |
|
|
||||||
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | **NEW** Styles for EnvelopeViewer.razor (external CSS, not inline). |
|
|
||||||
| `ReceiverUI/wwwroot/fake-data/annotations.json` | Dev-mode fake annotations (YARP proxy target). |
|
|
||||||
| `ReceiverUI/Models/AnnotationDto.cs` | Annotation position model. All properties non-nullable. |
|
|
||||||
| `ReceiverUI/Services/AnnotationService.cs` | Fetches `List<AnnotationDto>` from API or fake-data. |
|
|
||||||
| `ReceiverUI/Services/DocumentService.cs` | Fetches PDF bytes from API. |
|
|
||||||
| `ReceiverUI/Services/AuthService.cs` | Manages receiver session cookie. |
|
|
||||||
| `API/Controllers/AnnotationController.cs` | GET `api/Annotation/{key}` ? annotation list. |
|
|
||||||
| `API/Controllers/DocumentController.cs` | GET `api/Document/{key}` ? PDF bytes. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SignatureDto / AnnotationDto — Coordinate System
|
|
||||||
|
|
||||||
**Database Storage Format:** INCHES (GdPicture14 native unit)
|
|
||||||
**Origin:** Top-left corner of page
|
|
||||||
**Axes:** X increases rightward, Y increases downward
|
|
||||||
|
|
||||||
### Source Evidence (VB.NET Legacy Code)
|
|
||||||
|
|
||||||
```vb
|
|
||||||
' From: EnvelopeGenerator.Form/frmFieldEditor.vb
|
|
||||||
' GdPicture14.Annotations.AnnotationStickyNote
|
|
||||||
|
|
||||||
'Breite und Höhe in Inches (4,5*5cm)
|
|
||||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' 1.77 inches = 4.5cm
|
|
||||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' 1.96 inches = 5cm
|
|
||||||
|
|
||||||
Sub LoadAnnotation(pElement As Signature, ...)
|
|
||||||
oAnnotation.Left = CSng(pElement.X) ' Direct assignment ? INCHES
|
|
||||||
oAnnotation.Top = CSng(pElement.Y)
|
|
||||||
oAnnotation.Width = CSng(pElement.Width)
|
|
||||||
oAnnotation.Height = CSng(pElement.Height)
|
|
||||||
End Sub
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conversion Formulas
|
|
||||||
|
|
||||||
```
|
|
||||||
Inches ? DevExpress (DX): x_DX = x_inches * 100.0
|
|
||||||
y_DX = y_inches * 100.0
|
|
||||||
|
|
||||||
Inches ? PDF Points: x_pt = x_inches * 72.0
|
|
||||||
y_pt = x_inches * 72.0
|
|
||||||
|
|
||||||
Inches ? PDF.js Canvas: normalize to [0,1], then scale to pixels
|
|
||||||
x_norm = x_inches / pageWidth_inches
|
|
||||||
y_norm = y_inches / pageHeight_inches
|
|
||||||
x_px = x_norm * canvasWidth * scale * dpr
|
|
||||||
y_px = y_norm * canvasHeight * scale * dpr
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Comparison Table
|
|
||||||
|
|
||||||
| System | Unit | Origin | Conversion from INCHES |
|
|
||||||
|--------|------|--------|------------------------|
|
|
||||||
| **GdPicture14** (Source) | **Inches** | Top-left | Database format (no conversion) |
|
|
||||||
| DevExpress (LEGACY) | 1/100 inch (DX) | Top-left | `x_DX = x_inches * 100` |
|
|
||||||
| PDF.js (NEW) | Pixels | Top-left | `normalize ? scale` |
|
|
||||||
| PDF Points (iText7) | Points (1/72") | **Bottom-left** | `x_pt = x_inches * 72` + Y-flip |
|
|
||||||
| PSPDFKit (Web) | Points (1/72") | Top-left | `x_pt = x_inches * 72` |
|
|
||||||
|
|
||||||
**A4 Page Dimensions:**
|
|
||||||
- Width: 8.27 inches = 595 points = 827 DX units
|
|
||||||
- Height: 11.69 inches = 842 points = 1169 DX units
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer
|
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
|
||||||
**Purpose:** Modern, high-performance PDF viewing without signing functionality.
|
|
||||||
**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
**Blazor Component (`EnvelopeViewer.razor`):**
|
|
||||||
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
|
|
||||||
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
|
||||||
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
|
|
||||||
- Quality settings loaded from `appsettings.json` via `IOptions<PdfViewerOptions>`
|
|
||||||
- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence)
|
|
||||||
- CSS externalized to `envelope-viewer.css`
|
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
|
||||||
```javascript
|
|
||||||
window.pdfViewer = {
|
|
||||||
qualityOptions, // Configurable from appsettings.json
|
|
||||||
setQualityOptions(options), // Dynamic quality update
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
|
||||||
renderPage(num),
|
|
||||||
renderThumbnail(pageNum, canvasId),
|
|
||||||
attachWheelEvent(), // Ctrl+Wheel zoom (configurable step)
|
|
||||||
zoomIn(), zoomOut(), // Configurable step percentage
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Options (`PdfViewerOptions.cs`):**
|
|
||||||
```csharp
|
|
||||||
public class PdfViewerOptions {
|
|
||||||
public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5
|
|
||||||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
|
||||||
public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0
|
|
||||||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
|
||||||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
|
||||||
public bool EnableSmoothZoom { get; set; } = true;
|
|
||||||
public int ZoomTransitionDuration { get; set; } = 150; // ms
|
|
||||||
public double RenderingOpacity { get; set; } = 0.85; // 0.0-1.0
|
|
||||||
public int ThumbnailRenderDelay { get; set; } = 50; // ms
|
|
||||||
public int ZoomStepPercentage { get; set; } = 5; // 1-50%
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration (appsettings.json)
|
|
||||||
|
|
||||||
**Location:** `EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"PdfViewer": {
|
|
||||||
"ThumbnailBaseScale": 0.75,
|
|
||||||
"ThumbnailEnableHiDPI": true,
|
|
||||||
"ThumbnailMaxDPR": 2.0,
|
|
||||||
"MainCanvasEnableHiDPI": true,
|
|
||||||
"MainCanvasMaxDPR": 2.0,
|
|
||||||
"EnableSmoothZoom": true,
|
|
||||||
"ZoomTransitionDuration": 150,
|
|
||||||
"RenderingOpacity": 0.85,
|
|
||||||
"ThumbnailRenderDelay": 50,
|
|
||||||
"ZoomStepPercentage": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:** Edit file ? F5 (browser refresh) ? new settings applied
|
|
||||||
|
|
||||||
**Presets:**
|
|
||||||
- **High Quality**: `ThumbnailBaseScale: 1.0, MaxDPR: 3.0` (powerful devices)
|
|
||||||
- **Balanced**: Default values (recommended)
|
|
||||||
- **Performance**: `ThumbnailBaseScale: 0.5, EnableHiDPI: false` (mobile/low-end)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
1. **HiDPI/Retina Support** ? 4x quality on Retina displays
|
|
||||||
2. **Configurable Quality** ? All parameters in appsettings.json
|
|
||||||
3. **Unlimited Zoom** ? 50%-300%, configurable step (default 5%)
|
|
||||||
4. **Global Ctrl+Wheel Zoom** ? Works anywhere on page
|
|
||||||
5. **Thumbnail Sidebar** ? Resizable (150-400px), high-quality rendering
|
|
||||||
6. **Smooth Transitions** ? Configurable fade effect
|
|
||||||
7. **Responsive Design** ? Desktop/mobile adaptive layout
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
1. **Unlimited Zoom:**
|
|
||||||
- Canvas size not restricted by `max-width`
|
|
||||||
- Frame stays fixed, scroll bars appear automatically
|
|
||||||
- `text-align: center` for small sizes, full scroll for zoomed views
|
|
||||||
|
|
||||||
2. **Global Mouse Wheel Zoom:**
|
|
||||||
- Event listener on `document.body` (works anywhere on page)
|
|
||||||
- `Ctrl + Mouse Wheel` triggers `zoomIn()`/`zoomOut()`
|
|
||||||
- Calls `dotNetReference.invokeMethodAsync('OnZoomChanged', scale)` to update Blazor UI
|
|
||||||
- `{ passive: false }` to enable `preventDefault()`
|
|
||||||
|
|
||||||
3. **Render Task Cancellation:**
|
|
||||||
- Stores `currentRenderTask` to cancel previous render if new one starts
|
|
||||||
- Catches `RenderingCancelledException` to avoid console errors
|
|
||||||
- Queue system (`pageNumPending`) for rapid page changes
|
|
||||||
|
|
||||||
4. **Thumbnail Sidebar:**
|
|
||||||
- Left panel with page previews (sequential rendering, 50ms delay)
|
|
||||||
- Click to navigate to specific page
|
|
||||||
- Active page highlighted with gradient border
|
|
||||||
- No header/title (maximizes thumbnail space)
|
|
||||||
- Toggle button in toolbar to show/hide
|
|
||||||
|
|
||||||
5. **Resizable Splitter:**
|
|
||||||
- 4px draggable divider between thumbnails and canvas
|
|
||||||
- Min width: 150px, Max width: 400px
|
|
||||||
- Visual feedback: gradient on hover/active
|
|
||||||
- User preference saved to localStorage (`envelopeViewer_thumbnailWidth`)
|
|
||||||
- Global mouse events (works anywhere during drag)
|
|
||||||
- `col-resize` cursor (?) for intuitive UX
|
|
||||||
|
|
||||||
6. **Flex Layout:**
|
|
||||||
- Thumbnails and canvas in same container (`.pdf-frame`)
|
|
||||||
- `display: flex, flex-direction: row, align-items: stretch`
|
|
||||||
- Perfect vertical alignment (same top/bottom position)
|
|
||||||
- Responsive: column layout on mobile (<768px)
|
|
||||||
|
|
||||||
7. **Responsive Design:**
|
|
||||||
- Desktop: 90% width, 1200px max
|
|
||||||
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
|
|
||||||
- Adaptive padding and font sizes
|
|
||||||
|
|
||||||
### Initialization Flow
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
OnInitializedAsync():
|
|
||||||
1. Fetch PDF bytes from DocumentService
|
|
||||||
2. Convert to base64 data URL
|
|
||||||
3. Set _isLoading = false
|
|
||||||
|
|
||||||
OnAfterRenderAsync(firstRender):
|
|
||||||
1. Load saved thumbnail width from localStorage
|
|
||||||
2. Create DotNetObjectReference
|
|
||||||
3. Send PdfViewerOptions to JavaScript
|
|
||||||
4. Initialize PDF.js viewer
|
|
||||||
5. Attach splitter resize listeners
|
|
||||||
6. Render thumbnails sequentially (configurable delay)
|
|
||||||
```
|
|
||||||
|
|
||||||
**User Interactions:**
|
|
||||||
- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage
|
|
||||||
- Pages: Buttons/Input/Thumbnails ? navigate
|
|
||||||
- Sidebar: Toggle button ? show/hide thumbnails
|
|
||||||
- Splitter: Drag ? resize sidebar (150-400px)
|
|
||||||
|
|
||||||
**Cleanup:**
|
|
||||||
```csharp
|
|
||||||
DisposeAsync():
|
|
||||||
- Dispose PDF.js viewer
|
|
||||||
- Detach event listeners
|
|
||||||
- Dispose DotNetObjectReference
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Key Differences from ReportViewer
|
|
||||||
|
|
||||||
| Feature | EnvelopeViewer (NEW) | ReportViewer (LEGACY) |
|
|
||||||
|---------|----------------------|------------------------|
|
|
||||||
| Technology | PDF.js + Canvas | DevExpress XtraReports |
|
|
||||||
| Route | `/envelope/{key}` | `/receiver/{key}` |
|
|
||||||
| Purpose | Read-only viewing | Signature workflow |
|
|
||||||
| Dependencies | PDF.js CDN | DevExpress NuGet packages |
|
|
||||||
| Zoom | Unlimited, smooth | Report viewer default |
|
|
||||||
| Mouse Wheel | Custom Ctrl+Wheel | Browser default |
|
|
||||||
| File Size | Minimal (JS + CSS) | Heavy (DX libs) |
|
|
||||||
| Maintenance | Simple, standard web | Complex, vendor-specific |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ReceiverUI Signing Flow (ReportViewer.razor — LEGACY)
|
|
||||||
|
|
||||||
### On Load (`OnInitializedAsync`)
|
|
||||||
1. `AuthService.CheckEnvelopeAccessAsync` ? redirect to login if unauthorized
|
|
||||||
2. `AnnotationService.GetAnnotationsAsync` ? fills `_annotations`
|
|
||||||
3. `DocumentService.GetDocumentAsync` ? fills `_basePdfBytes` (real mode)
|
|
||||||
4. `BuildFreshBaseReport()` ? `XtraReport` for `DxReportViewer`
|
|
||||||
|
|
||||||
### Signature Popup
|
|
||||||
- Tabs: Draw / Text / Image
|
|
||||||
- Fields: full name (required), position (optional), place (required)
|
|
||||||
- Saved to `_capturedSignature` record
|
|
||||||
- If annotations exist ? popup closes ? JS checkbox overlays installed
|
|
||||||
|
|
||||||
### JS Checkbox Overlay (`receiver-signature.js`)
|
|
||||||
- `receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)`
|
|
||||||
- One `.annot-sig-cb-wrapper` div per annotation, absolutely positioned over viewer scroll container
|
|
||||||
- Position: `left = pageRect.left + ann.x * scaleX`, `top = pageRect.top + ann.y * scaleY`
|
|
||||||
- `scaleX = pagePixelWidth / 827`, `scaleY = pagePixelHeight / 1169`
|
|
||||||
- Click ? `dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)`
|
|
||||||
|
|
||||||
### Apply Signatures ("Unterschriften anwenden" — `SubmitSignaturesAsync`)
|
|
||||||
|
|
||||||
**Real PDF mode (`_basePdfBytes` is set):**
|
|
||||||
- Calls `StampSignaturesOnPdf` using **iText7** directly on PDF bytes
|
|
||||||
- Coordinate conversion: `xPt = ann.X * (72/100)`, `yPt = pageHeight - ann.Y * (72/100) - sigHeight` (Y flip: DX top-down ? PDF bottom-up)
|
|
||||||
- Returns stamped bytes ? loaded into new `XtraReport` with `XRPdfContent`
|
|
||||||
- Viewer refreshed with `ViewerKey++`
|
|
||||||
|
|
||||||
**Dev/fake mode (`_basePdfBytes` is null):**
|
|
||||||
- Falls back to `AddSignatureAtAnnotation` (XtraReports DetailBand + BeforePrint counter)
|
|
||||||
- This is intentionally left as a dev fallback only
|
|
||||||
|
|
||||||
### Export
|
|
||||||
- `reportViewer.ExportToAsync(ExportFormat.Pdf)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## StampSignaturesOnPdf — iText7 Implementation
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Located in ReportViewer.razor @code section
|
|
||||||
// Called by SubmitSignaturesAsync when _basePdfBytes is available
|
|
||||||
|
|
||||||
static byte[] StampSignaturesOnPdf(
|
|
||||||
byte[] sourcePdfBytes, byte[] signatureImageBytes,
|
|
||||||
string signerFullName, string signerPosition, string signaturePlace,
|
|
||||||
IReadOnlyList<AnnotationDto> annotations)
|
|
||||||
{
|
|
||||||
// Opens PDF with PdfReader/PdfWriter
|
|
||||||
// For each annotation:
|
|
||||||
// pageNum = ann.Page (clamped to totalPages)
|
|
||||||
// xPt = ann.X * (72f/100f)
|
|
||||||
// imgBottomY = pageHeight - ann.Y * (72f/100f) - sigHeightPt ? Y-axis flip
|
|
||||||
// PdfCanvas.AddImageFittedIntoRectangle(imageData, rect, false)
|
|
||||||
// Separator line at bottom of image
|
|
||||||
// Canvas text block below separator (font: Helvetica 7pt, color: RGB(73,80,87))
|
|
||||||
// Returns stamped byte[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BuildFreshBaseReport()
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Real PDF mode:
|
|
||||||
var report = new XtraReport();
|
|
||||||
var detail = new DetailBand();
|
|
||||||
report.Bands.Add(detail);
|
|
||||||
detail.Controls.Add(new XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
|
||||||
return report;
|
|
||||||
|
|
||||||
// Dev/fake mode: returns pre-built report from ReportStorage
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NuGet Packages in ReceiverUI
|
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `DevExpress.Blazor.Reporting.Viewer` | 25.2.3 | DxReportViewer (LEGACY, used in ReportViewer.razor) |
|
|
||||||
| `DevExpress.Blazor.PdfViewer` | 25.2.3 | PDF viewer (not used in EnvelopeViewer) |
|
|
||||||
| `DevExpress.Drawing.Skia` | 25.2.3 | Drawing backend |
|
|
||||||
| `itext` | 8.0.5 | PDF stamping (iText7) |
|
|
||||||
| `SkiaSharp.*` | 3.119.1 | WASM native rendering |
|
|
||||||
|
|
||||||
**External CDN (EnvelopeViewer):**
|
|
||||||
- PDF.js 3.11.174 (via `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`)
|
|
||||||
- PDF.js Worker (`pdf.worker.min.js`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
|
||||||
|
|
||||||
| Mistake | Why Wrong | Session |
|
|
||||||
|---|---|---|
|
|
||||||
| `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong | 4 |
|
|
||||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages | 8 |
|
|
||||||
| `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` | 5 |
|
|
||||||
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM | — |
|
|
||||||
| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI | — |
|
|
||||||
| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly | 10 |
|
|
||||||
| **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | **11** |
|
|
||||||
| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | **11** |
|
|
||||||
| **Toolbar: Complex left/center/right layout** | **User wants simple horizontal layout; failed multiple times to implement** | **11** |
|
|
||||||
| **Zoom label: Badge style (gradient/border/padding)** | **Over-designed; user prefers simple text label** | **11** |
|
|
||||||
| **Attempting to "improve" simple designs** | **User requests simplicity; AI keeps over-engineering** | **11** |
|
|
||||||
| **Ignoring explicit "revert" instructions** | **User said revert toolbar, AI tried to fix CSS instead of reverting HTML structure** | **11** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DevExpress Article (2023-08-28) — Why It Does NOT Apply
|
|
||||||
|
|
||||||
The article describes **X.509 cryptographic digital signatures** via `PdfDocumentSigner` + `Pkcs7Signer`.
|
|
||||||
Our use case is **visual/image stamping** at specific page coordinates — different problem, different API.
|
|
||||||
`XRPdfSignature` in the article requires pre-placed fields in the report designer, not runtime coordinates.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Layout Architecture (EnvelopeViewer)
|
|
||||||
|
|
||||||
### HTML Structure
|
|
||||||
```html
|
|
||||||
<div class="pdf-viewer-container">
|
|
||||||
<div class="pdf-toolbar">
|
|
||||||
<!-- Zoom, page navigation, thumbnail toggle -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-frame">
|
|
||||||
@if (_showThumbnails) {
|
|
||||||
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
|
||||||
<!-- Page previews -->
|
|
||||||
</div>
|
|
||||||
<div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
|
|
||||||
<!-- Resizable divider -->
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="pdf-canvas-wrapper">
|
|
||||||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### CSS Flexbox Layout
|
|
||||||
```css
|
|
||||||
.pdf-frame {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row; /* Side-by-side */
|
|
||||||
align-items: stretch; /* Same height */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-thumbnails {
|
|
||||||
flex-shrink: 0; /* Fixed width */
|
|
||||||
width: 260px; /* Dynamic via inline style */
|
|
||||||
border-right: none; /* Seamless join with splitter */
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-splitter {
|
|
||||||
width: 4px;
|
|
||||||
cursor: col-resize;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-canvas-wrapper {
|
|
||||||
flex: 1; /* Fill remaining space */
|
|
||||||
overflow: auto; /* Scrollable for zoom */
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resizable Splitter Workflow
|
|
||||||
```
|
|
||||||
1. User hovers splitter ? cursor: col-resize (?)
|
|
||||||
2. Mouse down ? OnSplitterMouseDown(e)
|
|
||||||
- _isResizing = true
|
|
||||||
- Store start position (clientX) and width
|
|
||||||
- Add 'resizing' class to body (prevent text selection)
|
|
||||||
- Call pdfViewer.startResize()
|
|
||||||
|
|
||||||
3. Mouse move (global) ? OnSplitterMouseMove(clientX)
|
|
||||||
- Calculate delta = clientX - startX
|
|
||||||
- newWidth = startWidth + delta
|
|
||||||
- Clamp to 150-400px range
|
|
||||||
- Update _thumbnailWidth
|
|
||||||
- StateHasChanged() for reactive UI
|
|
||||||
|
|
||||||
4. Mouse up (global) ? OnSplitterMouseUp()
|
|
||||||
- _isResizing = false
|
|
||||||
- Remove 'resizing' class
|
|
||||||
- Save to localStorage("envelopeViewer_thumbnailWidth")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Responsive Behavior
|
|
||||||
- **Desktop (>768px)**: Flex row, side-by-side
|
|
||||||
- **Mobile (?768px)**: Flex column, thumbnails on top
|
|
||||||
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Signature Buttons in EnvelopeViewer — Interactive Overlay System
|
|
||||||
|
|
||||||
**Purpose:** Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database.
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
**Blazor Component (`EnvelopeViewer.razor`):**
|
|
||||||
```csharp
|
|
||||||
IReadOnlyList<SignatureDto> _signatures = [];
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
var signatures = await SignatureService.GetAsync(EnvelopeKey);
|
|
||||||
_signatures = signatures.Convert(UnitOfLength.Point); // INCHES ? POINTS
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RenderSignatureButtonsAsync() {
|
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons",
|
|
||||||
_signatures, _currentPage, _dotNetRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public void OnSignatureButtonClick(int signatureId) {
|
|
||||||
Console.WriteLine($"Signature #{signatureId} signed");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
|
||||||
```javascript
|
|
||||||
renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
|
||||||
this.clearSignatureButtons(); // Remove old buttons
|
|
||||||
|
|
||||||
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
|
|
||||||
const signatureLayer = document.getElementById('pdf-signature-layer');
|
|
||||||
|
|
||||||
pageSignatures.forEach(sig => {
|
|
||||||
// Convert POINTS to display pixels
|
|
||||||
const xPx = sig.x * this.scale; // sig.x already in PDF POINTS
|
|
||||||
const yPx = sig.y * this.scale;
|
|
||||||
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.className = 'signature-button';
|
|
||||||
button.style.left = `${xPx}px`;
|
|
||||||
button.style.top = `${yPx}px`;
|
|
||||||
button.style.width = '150px';
|
|
||||||
button.style.height = '60px';
|
|
||||||
|
|
||||||
// German text + pen icon
|
|
||||||
button.innerHTML = `
|
|
||||||
<div>Unterschreiben</div>
|
|
||||||
<svg>...</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
signatureLayer.appendChild(button);
|
|
||||||
this.signatureButtons.push(button);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSignatureButtons() {
|
|
||||||
this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn));
|
|
||||||
this.signatureButtons = [];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**HTML Structure:**
|
|
||||||
```html
|
|
||||||
<div class="pdf-page-container">
|
|
||||||
<canvas id="pdf-canvas"></canvas>
|
|
||||||
<div id="pdf-text-layer"></div>
|
|
||||||
<div id="pdf-signature-layer"></div> <!-- NEW -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**CSS (`envelope-viewer.css`):**
|
|
||||||
```css
|
|
||||||
.pdf-signature-layer {
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0; right: 0; bottom: 0;
|
|
||||||
overflow: visible;
|
|
||||||
pointer-events: none; /* Pass clicks through to canvas */
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-button {
|
|
||||||
pointer-events: auto; /* Re-enable for buttons */
|
|
||||||
position: absolute;
|
|
||||||
width: 150px; height: 60px;
|
|
||||||
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
/* Hover: scale(1.05), shadow, darker gradient */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rendering Triggers
|
|
||||||
|
|
||||||
1. **Initial Load:** After PDF renders (`OnAfterRenderAsync`)
|
|
||||||
2. **Page Change:** `NextPage()`, `PreviousPage()`, `GoToPageFromThumbnail()`
|
|
||||||
3. **Zoom Change:** `OnZoomChanged()` (re-calculates pixel positions)
|
|
||||||
|
|
||||||
### Coordinate Conversion Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Database (INCHES)
|
|
||||||
? SignatureService.GetAsync()
|
|
||||||
SignatureDto.X/Y (INCHES)
|
|
||||||
? .Convert(UnitOfLength.Point)
|
|
||||||
SignatureDto.X/Y (PDF POINTS, × 72)
|
|
||||||
? JavaScript
|
|
||||||
Display Pixels (× this.scale)
|
|
||||||
? CSS
|
|
||||||
button.style.left/top
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example Calculation:**
|
|
||||||
- Database: `X = 1.5 inches, Y = 2.0 inches`
|
|
||||||
- After conversion: `X = 108 points, Y = 144 points` (× 72)
|
|
||||||
- At scale 1.5: `X = 162px, Y = 216px`
|
|
||||||
- Button positioned at `left: 162px, top: 216px`
|
|
||||||
|
|
||||||
### Button Design
|
|
||||||
|
|
||||||
**Visual Spec:**
|
|
||||||
- **Size:** 150px × 60px
|
|
||||||
- **Background:** Purple gradient `#4F46E5` ? `#4338CA` (hover: darker)
|
|
||||||
- **Text:** "Unterschreiben" (18px, bold, white)
|
|
||||||
- **Icon:** Pen SVG (24px white)
|
|
||||||
- **Effects:**
|
|
||||||
- Hover: `scale(1.05)` + shadow `0 4px 12px rgba(79, 70, 229, 0.4)`
|
|
||||||
- Active: `scale(0.98)`
|
|
||||||
- Focus: `2px solid #7e22ce` outline
|
|
||||||
|
|
||||||
**Accessibility:**
|
|
||||||
- `tabindex="0"` for keyboard navigation
|
|
||||||
- Focus outline for keyboard users
|
|
||||||
- Click handler with semantic button element
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13-14)
|
|
||||||
|
|
||||||
**IMPORTANT: iText7 NOT USED in EnvelopeViewer**
|
|
||||||
- **Reason:** GPL license incompatibility (requires source code sharing)
|
|
||||||
- **Alternative:** Client-side signature overlay system (HTML + Canvas API)
|
|
||||||
- **Export:** Signatures are visual overlays only, NOT stamped on PDF bytes
|
|
||||||
- **Future:** Consider PSPDFKit or commercial PDF library for actual PDF stamping
|
|
||||||
|
|
||||||
### Signature Data Structure
|
|
||||||
|
|
||||||
**Captured Signature (`SignatureCaptureDto`):**
|
|
||||||
```csharp
|
|
||||||
// Model: EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs
|
|
||||||
public sealed record SignatureCaptureDto
|
|
||||||
{
|
|
||||||
public required string DataUrl { get; init; } // base64 PNG: "data:image/png;base64,iVBORw0KG..."
|
|
||||||
public required string FullName { get; init; } // Required: "Max Mustermann"
|
|
||||||
public string Position { get; init; } = string.Empty; // Optional: "Geschäftsführer"
|
|
||||||
public required string Place { get; init; } // Required: "Berlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage in components:
|
|
||||||
SignatureCaptureDto? _capturedSignature;
|
|
||||||
|
|
||||||
// Initialization with object initializer (required properties):
|
|
||||||
_capturedSignature = new SignatureCaptureDto
|
|
||||||
{
|
|
||||||
DataUrl = signatureDataUrl,
|
|
||||||
FullName = _signerFullName.Trim(),
|
|
||||||
Position = _signerPosition.Trim(),
|
|
||||||
Place = _signaturePlace.Trim()
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Applied Signature (HTML Overlay):**
|
|
||||||
```html
|
|
||||||
<div class="applied-signature" data-signature-id="42" style="left: 162px; top: 216px;">
|
|
||||||
<img src="data:image/png;base64,..." /> <!-- Signature image (max 70px height) -->
|
|
||||||
<div style="border-top: 1px solid #495057;"></div> <!-- Separator line -->
|
|
||||||
<div style="font-size: 9px; color: #495057;">
|
|
||||||
<strong>Max Mustermann</strong> <!-- Name (bold, #212529) -->
|
|
||||||
<br>Geschäftsführer <!-- Position (optional) -->
|
|
||||||
<br>Berlin, 26.01.2025 <!-- Place, Date (dd.MM.yyyy) -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complete Workflow (Session 14 Update)
|
|
||||||
|
|
||||||
**Step 1: Page Load & Automatic Popup**
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
// ... load PDF and signatures ...
|
|
||||||
|
|
||||||
// Open signature popup automatically
|
|
||||||
_activeSignatureTab = SignatureTabDraw;
|
|
||||||
_signaturePopupVisible = true;
|
|
||||||
_popupValidationMessage = null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Opens automatically on page load (no manual trigger needed)
|
|
||||||
- Cannot be closed manually (no X button, ESC disabled, no outside-click)
|
|
||||||
- User MUST create signature before viewing PDF
|
|
||||||
|
|
||||||
**Step 2: Signature Creation Popup (DxPopup)**
|
|
||||||
|
|
||||||
**Tabs:**
|
|
||||||
1. **Zeichnen (Draw):** Canvas-based signature pad (`receiver-signature.js`)
|
|
||||||
- Touch-friendly: `touch-action: none`
|
|
||||||
- Line width: 2.5px, black (`#111`)
|
|
||||||
- Canvas: 560×180px, rounded corners, shadow
|
|
||||||
|
|
||||||
2. **Text:** Type signature with font selection
|
|
||||||
- Fonts: Brush Script, Segoe Script, Lucida Handwriting, Comic Sans, Cursive
|
|
||||||
- Real-time preview on canvas
|
|
||||||
|
|
||||||
3. **Bild (Image):** Upload PNG/JPG/WebP
|
|
||||||
- File input with preview
|
|
||||||
- Auto-resize to fit canvas
|
|
||||||
|
|
||||||
**Required Fields:**
|
|
||||||
- ? **Vor- und Nachname** (Full Name) — Red asterisk `*`
|
|
||||||
- ? **Ort** (Place) — Red asterisk `*`
|
|
||||||
- ? **Position** — Optional, gray "(optional)" label
|
|
||||||
|
|
||||||
**Validation:**
|
|
||||||
```csharp
|
|
||||||
async Task SaveSignatureAsync() {
|
|
||||||
if (string.IsNullOrWhiteSpace(_signerFullName)) {
|
|
||||||
_popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(_signaturePlace)) {
|
|
||||||
_popupValidationMessage = "Bitte geben Sie den Ort ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
|
||||||
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
|
||||||
_popupValidationMessage = "Die Unterschrift ist erforderlich.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to session state
|
|
||||||
_capturedSignature = new(signatureDataUrl, _signerFullName.Trim(),
|
|
||||||
_signerPosition.Trim(), _signaturePlace.Trim());
|
|
||||||
_signaturePopupVisible = false;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Design (Modern & Clean):**
|
|
||||||
- **Tabs:** Purple active state (`#4F46E5`), 3px bottom border
|
|
||||||
- **Inputs:** 2px solid border (`#e9ecef`), 6px border-radius, consistent padding
|
|
||||||
- **Canvas:** Light shadow (`0 1px 3px rgba(0,0,0,0.1)`), rounded corners
|
|
||||||
- **Buttons:**
|
|
||||||
- **Erneuern:** Outline secondary with refresh icon
|
|
||||||
- **Speichern:** Purple gradient + checkmark icon + shadow
|
|
||||||
- **Error:** Red left border (`4px solid #dc3545`), light red background (`#fee`)
|
|
||||||
|
|
||||||
**Step 3: PDF Viewing with Signature Buttons**
|
|
||||||
|
|
||||||
After popup closes:
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
|
||||||
// ... PDF initialization ...
|
|
||||||
|
|
||||||
await RenderSignatureButtonsAsync(); // Render "Unterschreiben" buttons
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Buttons appear at signature field positions:**
|
|
||||||
- Purple gradient background
|
|
||||||
- "Unterschreiben" text + pen icon
|
|
||||||
- Hover: scale(1.05) + darker color
|
|
||||||
- Positioned using PDF POINTS ? display pixels conversion
|
|
||||||
|
|
||||||
**Step 4: Apply Signature (Click "Unterschreiben")**
|
|
||||||
|
|
||||||
**C# Handler:**
|
|
||||||
```csharp
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnSignatureButtonClick(int signatureId) {
|
|
||||||
if (_capturedSignature == null) return;
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature",
|
|
||||||
signatureId,
|
|
||||||
_capturedSignature.DataUrl,
|
|
||||||
_capturedSignature.FullName,
|
|
||||||
_capturedSignature.Position,
|
|
||||||
_capturedSignature.Place);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript Implementation:**
|
|
||||||
```javascript
|
|
||||||
async applySignature(signatureId, signatureDataUrl, fullName, position, place) {
|
|
||||||
// 1. Find and remove button
|
|
||||||
const button = this.signatureButtons.find(btn =>
|
|
||||||
btn.getAttribute('data-signature-id') == signatureId);
|
|
||||||
button.parentNode.removeChild(button);
|
|
||||||
|
|
||||||
// 2. Create signature container (German standard format)
|
|
||||||
const signatureContainer = document.createElement('div');
|
|
||||||
signatureContainer.className = 'applied-signature';
|
|
||||||
signatureContainer.style.position = 'absolute';
|
|
||||||
signatureContainer.style.left = button.style.left; // Same position as button
|
|
||||||
signatureContainer.style.top = button.style.top;
|
|
||||||
signatureContainer.style.width = '230px';
|
|
||||||
signatureContainer.style.backgroundColor = '#f8f9fa';
|
|
||||||
signatureContainer.style.border = '1px solid #dee2e6';
|
|
||||||
signatureContainer.style.borderRadius = '6px';
|
|
||||||
signatureContainer.style.padding = '12px';
|
|
||||||
|
|
||||||
// 3. Add signature image
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = signatureDataUrl;
|
|
||||||
img.style.width = '100%';
|
|
||||||
img.style.maxHeight = '70px';
|
|
||||||
img.style.objectFit = 'contain';
|
|
||||||
|
|
||||||
// 4. Add separator line (German standard)
|
|
||||||
const separator = document.createElement('div');
|
|
||||||
separator.style.borderTop = '1px solid #495057';
|
|
||||||
separator.style.marginTop = '6px';
|
|
||||||
separator.style.marginBottom = '8px';
|
|
||||||
|
|
||||||
// 5. Add text information
|
|
||||||
const today = new Date();
|
|
||||||
const dateStr = today.toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
|
||||||
}); // "26.01.2025"
|
|
||||||
|
|
||||||
const infoHtml = [
|
|
||||||
`<strong>${this.escapeHtml(fullName)}</strong>`,
|
|
||||||
position ? this.escapeHtml(position) : null,
|
|
||||||
`${this.escapeHtml(place)}, ${dateStr}`
|
|
||||||
].filter(x => x).join('<br>');
|
|
||||||
|
|
||||||
const info = document.createElement('div');
|
|
||||||
info.style.fontSize = '9px';
|
|
||||||
info.style.color = '#495057';
|
|
||||||
info.innerHTML = infoHtml;
|
|
||||||
|
|
||||||
// 6. Assemble and add to layer
|
|
||||||
signatureContainer.appendChild(img);
|
|
||||||
signatureContainer.appendChild(separator);
|
|
||||||
signatureContainer.appendChild(info);
|
|
||||||
|
|
||||||
document.getElementById('pdf-signature-layer').appendChild(signatureContainer);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**German Standard Layout:**
|
|
||||||
```
|
|
||||||
???????????????????????????????
|
|
||||||
? [Signature Image] ? ? Base64 PNG, max 70px height
|
|
||||||
? ?
|
|
||||||
??????????????????????????????? ? 1px separator (#495057)
|
|
||||||
? ?
|
|
||||||
? Max Mustermann (Bold) ? ? Name (font-weight: 600, #212529)
|
|
||||||
? Geschäftsführer ? ? Position (optional, normal weight)
|
|
||||||
? Berlin, 26.01.2025 ? ? Place, Date (dd.MM.yyyy)
|
|
||||||
? ?
|
|
||||||
???????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 5: Persistence & Re-rendering**
|
|
||||||
|
|
||||||
- **Zoom/Page Change:** Applied signatures re-render automatically
|
|
||||||
- **Session State:** `_capturedSignature` stored in Blazor component
|
|
||||||
- **Limitation:** Lost on page refresh (no server-side storage)
|
|
||||||
- **Future:** Export to PDF with actual byte stamping (requires non-GPL library)
|
|
||||||
|
|
||||||
**Security:**
|
|
||||||
```javascript
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text; // Browser auto-escapes
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Protects against XSS attacks from malicious input in Name/Position/Place fields.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Popup Design Specification
|
|
||||||
|
|
||||||
**DxPopup Properties:**
|
|
||||||
```razor
|
|
||||||
<DxPopup @bind-Visible="_signaturePopupVisible"
|
|
||||||
HeaderText="Unterschrift erstellen"
|
|
||||||
Width="620px"
|
|
||||||
MaxWidth="95vw"
|
|
||||||
ShowFooter="true" <!-- REQUIRED for buttons to appear -->
|
|
||||||
CloseOnOutsideClick="false"
|
|
||||||
ShowCloseButton="false" <!-- No X button -->
|
|
||||||
CloseOnEscape="false"> <!-- ESC disabled -->
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tab Design:**
|
|
||||||
- **Active Tab:** `border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;`
|
|
||||||
- **Inactive Tab:** `color: #6c757d;`
|
|
||||||
- **Tab Bar:** `border-bottom: 2px solid #e9ecef;`
|
|
||||||
|
|
||||||
**Input Styling:**
|
|
||||||
```css
|
|
||||||
input, select {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Canvas Styling:**
|
|
||||||
```css
|
|
||||||
canvas {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Button Styling:**
|
|
||||||
```css
|
|
||||||
/* Erneuern (Renew) */
|
|
||||||
.btn-outline-secondary {
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Speichern (Save) */
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Future Enhancement Required:**
|
|
||||||
- Replace iText7 with commercial PDF library (e.g., PSPDFKit, Syncfusion)
|
|
||||||
- Or use server-side stamping with Azure PDF Services
|
|
||||||
- Or accept GPL license and open-source the stamping code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
17
EnvelopeGenerator.API/AuthScheme.cs
Normal file
17
EnvelopeGenerator.API/AuthScheme.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using EnvelopeGenerator.Application.Common.Dto;
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
using EnvelopeGenerator.Application.Documents.Queries;
|
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
using EnvelopeGenerator.Application.Histories.Queries;
|
using EnvelopeGenerator.Application.Histories.Queries;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
@@ -13,7 +13,6 @@ using MediatR;
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Components.Forms;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
@@ -62,8 +61,8 @@ public class AnnotationController : ControllerBase
|
|||||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var signature = User.GetReceiverSignatureOfReceiver();
|
var signature = User.ReceiverSignature();
|
||||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
var uuid = User.EnvelopeUuid();
|
||||||
|
|
||||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
||||||
|
|
||||||
@@ -75,12 +74,24 @@ public class AnnotationController : ControllerBase
|
|||||||
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||||
|
|
||||||
var docSignedNotification = await _mediator
|
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
||||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
var docSignedNotification = envelopeReceiverDto is not null
|
||||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
||||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
: throw new NotFoundException("Envelope receiver is not found.");
|
||||||
|
|
||||||
await _mediator.PublishSafely(docSignedNotification, cancel);
|
try
|
||||||
|
{
|
||||||
|
await _mediator.Publish(docSignedNotification, cancel);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _mediator.Publish(new RemoveSignatureNotification()
|
||||||
|
{
|
||||||
|
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
|
||||||
|
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
|
||||||
|
}, cancel);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -95,9 +106,9 @@ public class AnnotationController : ControllerBase
|
|||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||||
{
|
{
|
||||||
var signature = User.GetReceiverSignatureOfReceiver();
|
var signature = User.ReceiverSignature();
|
||||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
var uuid = User.EnvelopeUuid();
|
||||||
var mail = User.GetReceiverMailOfReceiver();
|
var mail = User.ReceiverMail();
|
||||||
|
|
||||||
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
|||||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</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.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
public async Task<IActionResult> Logout()
|
public async Task<IActionResult> Logout()
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
|||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[HttpGet("check")]
|
[HttpGet("check")]
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
public IActionResult Check(string? role = null)
|
public IActionResult Check(string? role = null)
|
||||||
=> role is not null && !User.IsInRole(role)
|
=> role is not null && !User.IsInRole(role)
|
||||||
? Unauthorized()
|
? Unauthorized()
|
||||||
|
|||||||
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal file
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.API.Options;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages cached data for receivers using distributed cache.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
public class CacheController(
|
||||||
|
IDistributedCache cache,
|
||||||
|
IOptions<CacheOptions> cacheOptions) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a receiver's signature in cache for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> SaveSignature(
|
||||||
|
[FromRoute] string envelopeKey,
|
||||||
|
[FromBody] SignatureCacheRequest request,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
var json = JsonSerializer.Serialize(request);
|
||||||
|
|
||||||
|
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
|
||||||
|
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await cache.SetStringAsync(cacheKey, json, options ?? new DistributedCacheEntryOptions(), cancel);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a cached signature for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> GetSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
var json = await cache.GetStringAsync(cacheKey, cancel);
|
||||||
|
|
||||||
|
if (json is null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var signature = JsonSerializer.Deserialize<SignatureCacheRequest>(json);
|
||||||
|
return Ok(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a cached signature for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpDelete("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
await cache.RemoveAsync(cacheKey, cancel);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for caching signature data.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SignatureCacheRequest(
|
||||||
|
string DataUrl,
|
||||||
|
string FullName,
|
||||||
|
string Place,
|
||||||
|
string? Position = null);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -13,7 +14,7 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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 DocReceiverElementController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DocReceiverElementController(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
|
|||||||
if (query is not null)
|
if (query is not null)
|
||||||
return BadRequest("Query parameters are not allowed for receiver role.");
|
return BadRequest("Query parameters are not allowed for receiver role.");
|
||||||
|
|
||||||
var envelopeId = User.GetEnvelopeIdOfReceiver();
|
var envelopeId = User.EnvelopeId();
|
||||||
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
|
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
|
||||||
return receiverDoc.ByteData is byte[] receiverDocByte
|
return receiverDoc.ByteData is byte[] receiverDocByte
|
||||||
? File(receiverDocByte, "application/octet-stream")
|
? File(receiverDocByte, "application/octet-stream")
|
||||||
@@ -71,7 +71,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
|
|||||||
[HttpGet("{envelopeKey}")]
|
[HttpGet("{envelopeKey}")]
|
||||||
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
|
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
int envelopeId = User.GetEnvelopeIdOfReceiver();
|
int envelopeId = User.EnvelopeId();
|
||||||
|
|
||||||
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class EnvelopeController : ControllerBase
|
public class EnvelopeController : ControllerBase
|
||||||
{
|
{
|
||||||
|
public record DeleteEnvelopeRequest(string Reason);
|
||||||
|
|
||||||
private readonly ILogger<EnvelopeController> _logger;
|
private readonly ILogger<EnvelopeController> _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ public class EnvelopeController : ControllerBase
|
|||||||
/// <response code="401">Der Benutzer ist nicht authentifiziert.</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="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
||||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||||
{
|
{
|
||||||
@@ -108,4 +110,17 @@ public class EnvelopeController : ControllerBase
|
|||||||
else
|
else
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = id,
|
||||||
|
Reason = request?.Reason ?? string.Empty
|
||||||
|
}.WithAuth(User.GetId()));
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||||
|
|
||||||
foreach (var rcv in res.SentReceiver)
|
foreach (var rcv in res.SentReceiver)
|
||||||
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.Signatures ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.Signature>())
|
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.DocReceiverElementCreateDto>())
|
||||||
{
|
{
|
||||||
using SqlConnection conn = new(_cnnStr);
|
using SqlConnection conn = new(_cnnStr);
|
||||||
conn.Open();
|
conn.Open();
|
||||||
@@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
|
document.Id, rcv.Id, sign.Page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
#region Create history
|
#region Create history
|
||||||
// ENV_UID, STATUS_ID, USER_ID,
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
string sql_hist = @"
|
string sql_hist = @"
|
||||||
USE [DD_ECM]
|
|
||||||
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
@@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
|
envelope.Uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ public class ReadOnlyController : ControllerBase
|
|||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||||
{
|
{
|
||||||
var authReceiverMail = User.GetReceiverMailOfReceiver();
|
var authReceiverMail = User.ReceiverMail();
|
||||||
if (authReceiverMail is null)
|
if (authReceiverMail is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
var envelopeId = User.GetEnvelopeIdOfReceiver();
|
var envelopeId = User.EnvelopeId();
|
||||||
|
|
||||||
createDto.AddedWho = authReceiverMail;
|
createDto.AddedWho = authReceiverMail;
|
||||||
createDto.EnvelopeId = envelopeId;
|
createDto.EnvelopeId = envelopeId;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides custom claim types for envelope-related information.
|
|
||||||
/// </summary>
|
|
||||||
public static class EnvelopeClaimTypes
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Claim type for the title of an envelope.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly string Title = $"Envelope{nameof(Title)}";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Claim type for the ID of an envelope.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly string Id = $"Envelope{nameof(Id)}";
|
|
||||||
}
|
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.28" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
<PackageReference Include="itext" Version="8.0.5" />
|
<PackageReference Include="itext" Version="8.0.5" />
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Linq;
|
using DigitalData.Auth.Claims;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Extensions;
|
namespace EnvelopeGenerator.API.Extensions;
|
||||||
|
|
||||||
@@ -11,12 +9,14 @@ namespace EnvelopeGenerator.API.Extensions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ReceiverClaimExtensions
|
public static class ReceiverClaimExtensions
|
||||||
{
|
{
|
||||||
private static readonly string[] EnvelopeIdClaimTypes = [EnvelopeClaimTypes.Id, "envelope_id", "EnvelopeId"];
|
/// <summary>
|
||||||
private static readonly string[] ReceiverIdClaimTypes = ["receiver_id", "ReceiverId"];
|
///
|
||||||
private static readonly string[] EnvelopeUuidClaimTypes = [ClaimTypes.NameIdentifier, "envelope_uuid", "EnvelopeUuid"];
|
/// </summary>
|
||||||
private static readonly string[] ReceiverSignatureClaimTypes = [ClaimTypes.Hash, "receiver_sig", "ReceiverSignature"];
|
/// <param name="user"></param>
|
||||||
|
/// <param name="claimType"></param>
|
||||||
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, string claimType)
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
private static string GetRequiredClaimValue(this ClaimsPrincipal user, string claimType)
|
||||||
{
|
{
|
||||||
var value = user.FindFirstValue(claimType);
|
var value = user.FindFirstValue(claimType);
|
||||||
if (value is not null)
|
if (value is not null)
|
||||||
@@ -32,7 +32,7 @@ public static class ReceiverClaimExtensions
|
|||||||
throw new InvalidOperationException(message);
|
throw new InvalidOperationException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, params string[] claimTypes)
|
private static string GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes)
|
||||||
{
|
{
|
||||||
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
|
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
|
||||||
{
|
{
|
||||||
@@ -52,89 +52,45 @@ public static class ReceiverClaimExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authenticated envelope UUID from the claims.
|
/// Gets the authenticated envelope UUID from the claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetEnvelopeUuidOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeUuidClaimTypes);
|
public static string EnvelopeUuid(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authenticated receiver signature from the claims.
|
/// Gets the authenticated receiver signature from the claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetReceiverSignatureOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ReceiverSignatureClaimTypes);
|
public static string ReceiverSignature(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature);
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver display name from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetReceiverNameOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Name);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authenticated receiver email address from the claims.
|
/// Gets the authenticated receiver email address from the claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetReceiverMailOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Email);
|
public static string ReceiverMail(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope title from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetEnvelopeTitleOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Title);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authenticated envelope identifier from the claims.
|
/// Gets the authenticated envelope identifier from the claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static int GetEnvelopeIdOfReceiver(this ClaimsPrincipal user)
|
public static int EnvelopeId(this ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeIdClaimTypes);
|
var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId);
|
||||||
if (!int.TryParse(envIdStr, out var envId))
|
if (int.TryParse(envIdStr, out var envId))
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Claim '{"envelope_id"}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return envId;
|
return envId;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets the authenticated receiver identifier from the claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
public static int GetReceiverIdOfReceiver(this ClaimsPrincipal user)
|
public static int ReceiverId(this ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
var rcvIdStr = user.GetRequiredClaimOfReceiver(ReceiverIdClaimTypes);
|
var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
|
||||||
if (!int.TryParse(rcvIdStr, out var rcvId))
|
if (int.TryParse(rcvIdStr, out var rcvId))
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Claim '{"receiver_id"}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return rcvId;
|
return rcvId;
|
||||||
}
|
else
|
||||||
|
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
|
||||||
/// <summary>
|
|
||||||
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The current HTTP context.</param>
|
|
||||||
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
|
|
||||||
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
|
|
||||||
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
|
|
||||||
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
|
|
||||||
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
|
|
||||||
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
|
|
||||||
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
|
|
||||||
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
|
|
||||||
new(ClaimTypes.Role, receiverRole)
|
|
||||||
};
|
|
||||||
|
|
||||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
var authProperties = new AuthenticationProperties
|
|
||||||
{
|
|
||||||
AllowRefresh = false,
|
|
||||||
IsPersistent = false
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.SignInAsync(
|
|
||||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
|
||||||
new ClaimsPrincipal(claimsIdentity),
|
|
||||||
authProperties);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,6 +69,11 @@ public class ExceptionHandlingMiddleware
|
|||||||
message = notFoundEx.Message;
|
message = notFoundEx.Message;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ForbiddenException forbiddenEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||||
|
message = forbiddenEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.LogError(exception, "Unhandled exception occurred.");
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
|||||||
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal file
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for distributed caching.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CacheOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration section name in appsettings.json.
|
||||||
|
/// </summary>
|
||||||
|
public const string SectionName = "Cache";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signature cache expiration time.
|
||||||
|
/// If null, signatures will not expire automatically.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? SignatureCacheExpiration { get; set; }
|
||||||
|
}
|
||||||
@@ -13,13 +13,15 @@ using EnvelopeGenerator.Application;
|
|||||||
using DigitalData.Auth.Client;
|
using DigitalData.Auth.Client;
|
||||||
using DigitalData.Core.Abstractions;
|
using DigitalData.Core.Abstractions;
|
||||||
using EnvelopeGenerator.API.Models;
|
using EnvelopeGenerator.API.Models;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
using EnvelopeGenerator.API.Middleware;
|
using EnvelopeGenerator.API.Middleware;
|
||||||
|
using EnvelopeGenerator.API.Options;
|
||||||
using NLog.Web;
|
using NLog.Web;
|
||||||
using NLog;
|
using NLog;
|
||||||
using DigitalData.Auth.Claims;
|
using DigitalData.Auth.Claims;
|
||||||
|
using EnvelopeGenerator.API;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
logger.Info("Logging initialized!");
|
logger.Info("Logging initialized!");
|
||||||
@@ -42,7 +44,11 @@ try
|
|||||||
|
|
||||||
var deferredProvider = new DeferredServiceProvider();
|
var deferredProvider = new DeferredServiceProvider();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||||
|
});
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||||
|
|
||||||
@@ -129,15 +135,12 @@ try
|
|||||||
|
|
||||||
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
|
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
|
||||||
|
|
||||||
// Scheme name used for per-envelope receiver JWT authentication.
|
|
||||||
const string EnvelopeReceiverScheme = "EnvelopeReceiverJwt";
|
|
||||||
|
|
||||||
builder.Services.AddAuthentication(options =>
|
builder.Services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
})
|
})
|
||||||
.AddJwtBearer(opt =>
|
.AddJwtBearer(AuthScheme.Sender, opt =>
|
||||||
{
|
{
|
||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
opt.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
@@ -175,7 +178,7 @@ try
|
|||||||
// last path segment of the request URL.
|
// last path segment of the request URL.
|
||||||
// This enables simultaneous authentication for multiple envelopes
|
// This enables simultaneous authentication for multiple envelopes
|
||||||
// within the same browser session.
|
// within the same browser session.
|
||||||
.AddJwtBearer(EnvelopeReceiverScheme, opt =>
|
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
||||||
{
|
{
|
||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
opt.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
@@ -239,19 +242,17 @@ try
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorizationBuilder()
|
builder.Services.AddAuthorizationBuilder()
|
||||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy =>
|
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
|
||||||
policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
.RequireRole(Role.Sender, Role.Receiver.Full)
|
||||||
.AddPolicy(AuthPolicy.Sender, policy =>
|
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
||||||
policy.RequireRole(Role.Sender))
|
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||||
// Per-envelope policy: uses the dedicated EnvelopeReceiverJwt scheme so it
|
.RequireRole(Role.Sender)
|
||||||
// never conflicts with the default JwtBearer scheme.
|
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||||
.AddPolicy(AuthPolicy.Receiver, policy =>
|
.AddPolicy(AuthPolicy.Receiver, policy => policy
|
||||||
policy
|
.AddAuthenticationSchemes(AuthScheme.Receiver)
|
||||||
.AddAuthenticationSchemes(EnvelopeReceiverScheme)
|
|
||||||
.RequireAuthenticatedUser()
|
.RequireAuthenticatedUser()
|
||||||
.RequireRole(Role.Receiver.Full, "receiver"))
|
.RequireRole(Role.Receiver.Full, "receiver"))
|
||||||
.AddPolicy(AuthPolicy.ReceiverTFA, policy =>
|
.AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA));
|
||||||
policy.RequireRole(Role.Receiver.TFA));
|
|
||||||
|
|
||||||
// User manager
|
// User manager
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
@@ -265,6 +266,20 @@ try
|
|||||||
// Localizer
|
// Localizer
|
||||||
builder.Services.AddCookieBasedLocalizer();
|
builder.Services.AddCookieBasedLocalizer();
|
||||||
|
|
||||||
|
// Cache options
|
||||||
|
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
|
||||||
|
|
||||||
|
// Distributed Cache - SQL Server
|
||||||
|
builder.Services.AddDistributedSqlServerCache(options =>
|
||||||
|
{
|
||||||
|
config.GetSection("Cache:SqlServer").Bind(options);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.ConnectionString))
|
||||||
|
{
|
||||||
|
options.ConnectionString = connStr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Envelope generator serives
|
// Envelope generator serives
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
builder.Services
|
builder.Services
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
||||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "sender",
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -174,6 +174,14 @@
|
|||||||
"Receiver": [],
|
"Receiver": [],
|
||||||
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
||||||
},
|
},
|
||||||
|
"Cache": {
|
||||||
|
"SignatureCacheExpiration": null,
|
||||||
|
"SqlServer": {
|
||||||
|
"ConnectionString": null,
|
||||||
|
"SchemaName": "dbo",
|
||||||
|
"TableName": "TBDD_CACHE"
|
||||||
|
}
|
||||||
|
},
|
||||||
"MainPageTitle": null,
|
"MainPageTitle": null,
|
||||||
"AnnotationParams": {
|
"AnnotationParams": {
|
||||||
"Background": {
|
"Background": {
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
{
|
{
|
||||||
"ReverseProxy": {
|
"ReverseProxy": {
|
||||||
"Routes": {
|
"Routes": {
|
||||||
"receiver-ui-receiver": {
|
"receiver-ui-root": {
|
||||||
"ClusterId": "receiver-ui",
|
"ClusterId": "receiver-ui",
|
||||||
"Order": 100,
|
"Order": 300,
|
||||||
"Match": {
|
"Match": {
|
||||||
"Path": "/receiver/{**catch-all}",
|
"Path": "/",
|
||||||
"Methods": [ "GET", "HEAD" ]
|
"Methods": [ "GET", "HEAD" ]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"receiver-ui-login": {
|
"Transforms": [
|
||||||
"ClusterId": "receiver-ui",
|
{ "PathSet": "/index.html" }
|
||||||
"Order": 100,
|
]
|
||||||
"Match": {
|
|
||||||
"Path": "/login/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"receiver-ui-sender": {
|
"receiver-ui-sender": {
|
||||||
"ClusterId": "receiver-ui",
|
"ClusterId": "receiver-ui",
|
||||||
@@ -23,7 +18,10 @@
|
|||||||
"Match": {
|
"Match": {
|
||||||
"Path": "/sender/{**catch-all}",
|
"Path": "/sender/{**catch-all}",
|
||||||
"Methods": [ "GET", "HEAD" ]
|
"Methods": [ "GET", "HEAD" ]
|
||||||
}
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"receiver-ui-envelope": {
|
"receiver-ui-envelope": {
|
||||||
"ClusterId": "receiver-ui",
|
"ClusterId": "receiver-ui",
|
||||||
@@ -36,6 +34,17 @@
|
|||||||
{ "PathSet": "/index.html" }
|
{ "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": {
|
"receiver-ui-blazor-framework": {
|
||||||
"ClusterId": "receiver-ui",
|
"ClusterId": "receiver-ui",
|
||||||
"Order": 50,
|
"Order": 50,
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing configuration settings.
|
/// Data Transfer Object representing configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ConfigDto
|
public class ConfigDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
public class DocReceiverElementDto : IDocReceiverElement
|
||||||
public class SignatureDto : ISignature
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the element.
|
/// Gets or sets the unique identifier of the element.
|
||||||
@@ -104,4 +102,24 @@ public class SignatureDto : ISignature
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
|
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? FullName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? Position { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? Place { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? Ink { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentDto
|
public class DocumentDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -31,5 +28,5 @@ public class DocumentDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<SignatureDto>? Elements { get; set; }
|
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentStatusDto
|
public class DocumentStatusDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using DigitalData.UserManager.Application.DTOs.User;
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeDto : IEnvelope
|
public record EnvelopeDto : IEnvelope
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -126,4 +125,14 @@ public record EnvelopeDto : IEnvelope
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<DocumentDto>? Documents { get; set; }
|
public IEnumerable<DocumentDto>? Documents { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverDto
|
public record EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -75,4 +74,13 @@ public record EnvelopeReceiverDto
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPhoneNumber { get; init; }
|
public bool HasPhoneNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether this receiver has signed the envelope.
|
||||||
|
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
|
||||||
|
/// </summary>
|
||||||
|
public bool Signed => Envelope?.Histories?.Any(h =>
|
||||||
|
h.Receiver?.Id == ReceiverId &&
|
||||||
|
h.Status == EnvelopeStatus.DocumentSigned
|
||||||
|
) ?? false;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="DateValid"></param>
|
/// <param name="DateValid"></param>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||||
DateTime DateValid)
|
DateTime DateValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
@@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||||
/// Contains information about the receiver, associated envelope, and audit details.
|
/// Contains information about the receiver, associated envelope, and audit details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyDto
|
public class EnvelopeReceiverReadOnlyDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeTypeDto
|
public class EnvelopeTypeDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record SmsResponse
|
public record SmsResponse
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Dynamic;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents PSPDFKit annotation data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Instant">Instant annotation data.</param>
|
||||||
|
/// <param name="Structured">Structured annotation data.</param>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
|
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ReceiverDto
|
public class ReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
65
EnvelopeGenerator.Application/Common/Dto/Signature.cs
Normal file
65
EnvelopeGenerator.Application/Common/Dto/Signature.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
|
||||||
|
/// </summary>
|
||||||
|
public sealed record UserPreferencesDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The active DevExpress theme name.
|
||||||
|
/// Allowed values: "blazing-berry", "fluent", "purple"
|
||||||
|
/// </summary>
|
||||||
|
public string ThemeName { get; set; } = "blazing-berry";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether dark mode is active.
|
||||||
|
/// Default: false (light mode).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDarkMode { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
|
||||||
|
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
|
||||||
|
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> GridLayouts { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authenticated user's display name ("{GivenName} {Surname}").
|
||||||
|
/// Populated by the server on GET responses from JWT claims.
|
||||||
|
/// NOT persisted in cache — injected at read time by UserPreferencesController.
|
||||||
|
/// Ignored on PUT requests.
|
||||||
|
/// </summary>
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static UserPreferencesDto Default { get; } = new UserPreferencesDto();
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
namespace EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
|
||||||
@@ -21,4 +22,21 @@ public static class AutoMapperAuditingExtensions
|
|||||||
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
||||||
where TDestination : IHasChangedWhen
|
where TDestination : IHasChangedWhen
|
||||||
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
=> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use IRepository")]
|
[Obsolete("Use IRepository")]
|
||||||
public interface IDocumentReceiverElementRepository : ICRUDRepository<Signature, int>
|
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public interface IDocumentReceiverElementService : IBasicCRUDService<SignatureDto, Signature, int>
|
public interface IDocumentReceiverElementService : IBasicCRUDService<DocReceiverElementDto, DocReceiverElement, int>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
|
||||||
|
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
|
||||||
|
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserPreferencesService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves preferences for the given user.
|
||||||
|
/// Always returns a valid object — never throws, never returns null.
|
||||||
|
/// Returns defaults if no entry exists in cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persists the given preferences for the user in the distributed cache.
|
||||||
|
/// Failures are non-fatal and silently ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="preferences">The preferences to persist.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the AutoMapper profile configuration for mapping between
|
/// Represents the AutoMapper profile configuration for mapping between
|
||||||
@@ -23,34 +25,39 @@ public class MappingProfile : Profile
|
|||||||
{
|
{
|
||||||
// Entity to DTO mappings
|
// Entity to DTO mappings
|
||||||
CreateMap<Config, ConfigDto>();
|
CreateMap<Config, ConfigDto>();
|
||||||
CreateMap<Signature, SignatureDto>();
|
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>();
|
CreateMap<Envelope, EnvelopeDto>()
|
||||||
|
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||||
|
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
CreateMap<Receiver, ReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||||
|
|
||||||
// DTO to Entity mappings
|
// DTO to Entity mappings
|
||||||
CreateMap<ConfigDto, Config>();
|
CreateMap<ConfigDto, Config>();
|
||||||
CreateMap<SignatureDto, Signature>();
|
CreateMap<DocReceiverElementDto, DocReceiverElement>();
|
||||||
|
CreateMap<Signature, DocReceiverElement>()
|
||||||
|
.ForMember(dest => dest.Ink, opt => opt.MapFrom(src => src.DataUrl.MapDataUrlToRequiredBytes()))
|
||||||
|
.MapChangedWhen();
|
||||||
CreateMap<DocumentStatusDto, DocumentStatus>();
|
CreateMap<DocumentStatusDto, DocumentStatus>();
|
||||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||||
CreateMap<EnvelopeDto, Envelope>();
|
CreateMap<EnvelopeDto, Envelope>();
|
||||||
CreateMap<DocumentDto, Document>();
|
CreateMap<DocumentDto, Document>();
|
||||||
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
||||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||||
.MapAddedWhen();
|
.MapAddedWhen();
|
||||||
|
|
||||||
@@ -1,88 +1,37 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Dynamic;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Notification raised when a document is signed by a receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Instant"></param>
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
/// <param name="Structured"></param>
|
public record DocSignedNotification : INotification, ISendMailNotification
|
||||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Original"></param>
|
|
||||||
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification, ISendMailNotification
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// The envelope receiver information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public required EnvelopeReceiverDto EnvelopeReceiver { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The PSPDFKit annotation data.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets the email template type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets the email address of the receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string EmailAddress => Receiver?.EmailAddress
|
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||||
?? throw new InvalidOperationException($"Receiver is null." +
|
?? throw new InvalidOperationException($"Receiver is null." +
|
||||||
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class DocSignedNotificationExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dto">The DTO to convert.</param>
|
|
||||||
/// <param name="psPdfKitAnnotation"></param>
|
|
||||||
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
|
||||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
|
|
||||||
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dtoTask"></param>
|
|
||||||
/// <param name="psPdfKitAnnotation"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
|
|
||||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="publisher"></param>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await publisher.Publish(notification, cancel);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await publisher.Publish(new RemoveSignatureNotification()
|
|
||||||
{
|
|
||||||
EnvelopeId = notification.EnvelopeId,
|
|
||||||
ReceiverId = notification.ReceiverId
|
|
||||||
}, cancel);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||||
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -8,6 +9,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||||
{
|
{
|
||||||
private const string BlankAnnotationJson = "{}";
|
private const string BlankAnnotationJson = "{}";
|
||||||
@@ -29,10 +31,11 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
|||||||
/// <param name="notification"></param>
|
/// <param name="notification"></param>
|
||||||
/// <param name="cancel"></param>
|
/// <param name="cancel"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
||||||
{
|
{
|
||||||
EnvelopeId = notification.EnvelopeId,
|
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||||
ReceiverId = notification.ReceiverId,
|
ReceiverId = notification.EnvelopeReceiver.ReceiverId,
|
||||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||||
: BlankAnnotationJson
|
: BlankAnnotationJson
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (notification.Receiver is null)
|
if (notification.EnvelopeReceiver.Receiver is null)
|
||||||
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
await _sender.Send(new CreateHistoryCommand()
|
await _sender.Send(new CreateHistoryCommand()
|
||||||
{
|
{
|
||||||
EnvelopeId = notification.EnvelopeId,
|
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||||
UserReference = notification.Receiver.EmailAddress,
|
UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress,
|
||||||
Status = EnvelopeStatus.DocumentSigned,
|
Status = EnvelopeStatus.DocumentSigned,
|
||||||
}, cancel);
|
}, cancel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
|||||||
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
||||||
{
|
{
|
||||||
emailOut.ReferenceString = notification.EmailAddress;
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
emailOut.ReferenceId = notification.ReceiverId;
|
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -42,11 +42,11 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
|||||||
{
|
{
|
||||||
var placeHolders = new Dictionary<string, string>()
|
var placeHolders = new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
{ "[NAME_RECEIVER]", notification.Name ?? string.Empty },
|
{ "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? string.Empty },
|
||||||
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
|
{ "[DOCUMENT_TITLE]", notification.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.Envelope.IsReadAndConfirm())
|
if (notification.EnvelopeReceiver.Envelope.IsReadAndConfirm())
|
||||||
{
|
{
|
||||||
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
||||||
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
|
||||||
|
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||||
|
{
|
||||||
|
public required Envelope Envelope { get; init; }
|
||||||
|
|
||||||
|
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||||
|
|
||||||
|
public required string Reason { get; init; }
|
||||||
|
|
||||||
|
public required string SenderReference { get; init; }
|
||||||
|
|
||||||
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||||
|
|
||||||
|
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||||
|
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||||
|
|
||||||
|
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||||
|
{
|
||||||
|
public SendDeletedMailHandler(
|
||||||
|
IRepository<EmailTemplate> tempRepo,
|
||||||
|
IRepository<EmailOut> emailOutRepo,
|
||||||
|
IOptions<MailParams> mailParamsOptions,
|
||||||
|
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||||
|
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||||
|
{
|
||||||
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
|
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||||
|
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||||
|
{
|
||||||
|
var senderName = notification.Envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
|
senderName = notification.SenderReference;
|
||||||
|
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||||
|
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||||
|
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||||
|
["[REASON]"] = notification.Reason
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Query;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
|
|||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
/// <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>
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
[NotMapped]
|
||||||
|
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
|
|||||||
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Raw => @"
|
public string Raw => @"
|
||||||
DECLARE @OUT_RECEIVER_ID int
|
DECLARE @OUT_RECEIVER_ID bigint
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||||
{0},
|
{0},
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using QRCoder;
|
using QRCoder;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application;
|
namespace EnvelopeGenerator.Application;
|
||||||
|
|
||||||
@@ -48,14 +51,30 @@ public static class DependencyInjection
|
|||||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||||
|
|
||||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||||
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||||
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
|
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
|
||||||
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
||||||
services.TryAddSingleton<QRCodeGenerator>();
|
services.TryAddSingleton<QRCodeGenerator>();
|
||||||
|
|
||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
|
// Register SignCommand pipeline behaviors in execution order
|
||||||
|
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
|
|
||||||
|
// 1. AnnotationBehavior - Saves annotations (executes second)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||||
|
|
||||||
|
// 2. DocStatusBehavior - Creates document status (executes third)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||||
|
|
||||||
|
// 3. HistoryBehavior - Records history (executes fourth)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||||
|
|
||||||
|
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that saves annotations.
|
||||||
|
/// Executes first in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
|
public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IRepository<ElementAnnotation> _repo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AnnotationBehavior"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
public AnnotationBehavior(IRepository<ElementAnnotation> repository)
|
||||||
|
{
|
||||||
|
_repo = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if(request.ReceiverAppType != ReceiverAppType.LegacyWeb)
|
||||||
|
if(request.PsPdfKitAnnotation is null)
|
||||||
|
return await next(cancel);
|
||||||
|
else
|
||||||
|
throw new BadRequestException("PsPdfKit Annotation are only supported for the legacy web receiver type.");
|
||||||
|
|
||||||
|
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
|
||||||
|
await _repo.CreateAsync(annot.Structured, cancel);
|
||||||
|
else
|
||||||
|
throw new BadRequestException("Annotation data is missing or invalid.");
|
||||||
|
|
||||||
|
return await next(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that creates document status.
|
||||||
|
/// Executes second in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
public class DocStatusBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private const string BlankAnnotationJson = "{}";
|
||||||
|
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
public DocStatusBehavior(ISender sender)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _sender.Send(new CreateDocStatusCommand()
|
||||||
|
{
|
||||||
|
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
|
||||||
|
ReceiverId = request.EnvelopeReceiver.ReceiverId,
|
||||||
|
Value = request.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||||
|
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||||
|
: BlankAnnotationJson
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that resolves and validates EnvelopeReceiver.
|
||||||
|
/// Executes FIRST in the signing process - before all other behaviors.
|
||||||
|
/// If EnvelopeReceiver is not provided, it queries the database using EnvelopeReceiverQueryBase parameters.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IRepository<EnvelopeReceiver> _erRepo;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="erRepo"></param>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
public EnvelopeReceiverResolutionBehavior(IRepository<EnvelopeReceiver> erRepo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_erRepo = erRepo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// If EnvelopeReceiver is not provided, query it from database
|
||||||
|
if (request.EnvelopeReceiver is null)
|
||||||
|
{
|
||||||
|
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
||||||
|
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||||
|
|
||||||
|
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that records history.
|
||||||
|
/// Executes third in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
public class HistoryBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
public HistoryBehavior(ISender sender)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.EnvelopeReceiver.Receiver is null)
|
||||||
|
throw new InvalidOperationException($"Receiver information is missing in the notification. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
|
await _sender.Send(new CreateHistoryCommand()
|
||||||
|
{
|
||||||
|
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
|
||||||
|
UserReference = request.EnvelopeReceiver.Receiver.EmailAddress,
|
||||||
|
Status = EnvelopeStatus.DocumentSigned,
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that creates document status.
|
||||||
|
/// Executes second in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
private readonly IRepository<DocReceiverElement> _elementRepo;
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
/// <param name="elementRepo"></param>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
public SaveSignatureBehavior(ISender sender, IRepository<DocReceiverElement> elementRepo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
_elementRepo = elementRepo;
|
||||||
|
_elementRepo = elementRepo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (request.ReceiverAppType == ReceiverAppType.LegacyWeb)
|
||||||
|
return await next(cancel);
|
||||||
|
else if(request.Signatures is not IEnumerable<Signature> signatures)
|
||||||
|
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||||
|
|
||||||
|
var elements = await _elementRepo
|
||||||
|
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
||||||
|
.Where(e => e.ReceiverId == request.Receiver.Id)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
foreach (var element in elements)
|
||||||
|
{
|
||||||
|
var signatures = request.Signatures.Where(s => s.Id == element.Id).ToList();
|
||||||
|
if(signatures.Count == 0)
|
||||||
|
throw new BadRequestException("No signature found for element with id {element.Id}.");
|
||||||
|
else if(signatures.Count > 1)
|
||||||
|
throw new BadRequestException("Multiple signatures found for element with id {element.Id}.");
|
||||||
|
|
||||||
|
await _elementRepo.UpdateAsync(signatures.First(), e => e.Id == element.Id, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that sends signed mail notification.
|
||||||
|
/// Executes LAST in the signing process - only if all previous behaviors succeed.
|
||||||
|
/// </summary>
|
||||||
|
public class SendSignedMailBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IRepository<EmailTemplate> _tempRepo;
|
||||||
|
private readonly IRepository<EmailOut> _emailOutRepo;
|
||||||
|
private readonly MailParams _mailParams;
|
||||||
|
private readonly DispatcherParams _dispatcherParams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tempRepo"></param>
|
||||||
|
/// <param name="emailOutRepo"></param>
|
||||||
|
/// <param name="mailParamsOptions"></param>
|
||||||
|
/// <param name="dispatcherParamsOptions"></param>
|
||||||
|
public SendSignedMailBehavior(
|
||||||
|
IRepository<EmailTemplate> tempRepo,
|
||||||
|
IRepository<EmailOut> emailOutRepo,
|
||||||
|
IOptions<MailParams> mailParamsOptions,
|
||||||
|
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||||
|
{
|
||||||
|
_tempRepo = tempRepo;
|
||||||
|
_emailOutRepo = emailOutRepo;
|
||||||
|
_mailParams = mailParamsOptions.Value;
|
||||||
|
_dispatcherParams = dispatcherParamsOptions.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var placeHolders = CreatePlaceHolders(request);
|
||||||
|
|
||||||
|
var temp = await _tempRepo
|
||||||
|
.Where(x => x.Name == EmailTemplateType.DocumentSigned.ToString())
|
||||||
|
.SingleOrDefaultAsync(cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Email template not found. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
|
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, _mailParams.Placeholders);
|
||||||
|
temp.Body = ReplacePlaceHolders(temp.Body, placeHolders, _mailParams.Placeholders);
|
||||||
|
|
||||||
|
var emailOut = new EmailOut
|
||||||
|
{
|
||||||
|
EmailAddress = request.EnvelopeReceiver.Receiver!.EmailAddress,
|
||||||
|
EmailBody = temp.Body,
|
||||||
|
EmailSubj = temp.Subject,
|
||||||
|
AddedWhen = DateTime.Now,
|
||||||
|
AddedWho = _dispatcherParams.AddedWho,
|
||||||
|
SendingProfile = _dispatcherParams.SendingProfile,
|
||||||
|
ReminderTypeId = _dispatcherParams.ReminderTypeId,
|
||||||
|
EmailAttmt1 = _dispatcherParams.EmailAttmt1,
|
||||||
|
WfId = (int)EnvelopeStatus.MessageConfirmationSent,
|
||||||
|
ReferenceString = request.EnvelopeReceiver.Receiver!.EmailAddress,
|
||||||
|
ReferenceId = request.EnvelopeReceiver.ReceiverId
|
||||||
|
};
|
||||||
|
|
||||||
|
await _emailOutRepo.CreateAsync(emailOut, cancellationToken);
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> CreatePlaceHolders(SigningCommand request)
|
||||||
|
{
|
||||||
|
var placeHolders = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "[NAME_RECEIVER]", request.EnvelopeReceiver.Name ?? string.Empty },
|
||||||
|
{ "[DOCUMENT_TITLE]", request.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.EnvelopeReceiver.Envelope.IsReadAndConfirm())
|
||||||
|
{
|
||||||
|
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
||||||
|
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
||||||
|
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
|
||||||
|
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
|
||||||
|
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
|
||||||
|
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
|
||||||
|
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
|
||||||
|
placeHolders["[FINAL_STATUS]"] = "Signatur";
|
||||||
|
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
|
||||||
|
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
|
||||||
|
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeHolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReplacePlaceHolders(string text, params Dictionary<string, string>[] placeHoldersList)
|
||||||
|
{
|
||||||
|
foreach (var placeHolders in placeHoldersList)
|
||||||
|
foreach (var ph in placeHolders)
|
||||||
|
text = text.Replace(ph.Key, ph.Value);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to sign a document by a receiver.
|
||||||
|
/// </summary>
|
||||||
|
public record SigningCommand : EnvelopeReceiverQueryBase, IRequest
|
||||||
|
{
|
||||||
|
private EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
|
|
||||||
|
internal void SetEnvelopeReceiver(EnvelopeReceiverDto envelopeReceiver)
|
||||||
|
{
|
||||||
|
_envelopeReceiver = envelopeReceiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The envelope receiver information.
|
||||||
|
/// </summary>
|
||||||
|
public EnvelopeReceiverDto EnvelopeReceiver
|
||||||
|
{
|
||||||
|
get => _envelopeReceiver!;
|
||||||
|
init => _envelopeReceiver = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The PSPDFKit annotation data.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
|
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Signature>? Signatures { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public ReceiverAppType ReceiverAppType { get; init; } = ReceiverAppType.ReceiverUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the sign command. All work is done by pipeline behaviors.
|
||||||
|
/// This handler is intentionally empty - behaviors handle all the processing.
|
||||||
|
/// </summary>
|
||||||
|
public class SignCommandHandler : IRequestHandler<SigningCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the signing command. Pipeline behaviors handle all processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task Handle(SigningCommand request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public enum ReceiverAppType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
ReceiverUI = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
LegacyWeb = 1,
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceiverElementQuery, IEnumerable<DocReceiverElementDto>>
|
||||||
|
{
|
||||||
|
private readonly IRepository<DocReceiverElement> _repository;
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
public ReadDocReceiverElementQueryHandler(IRepository<DocReceiverElement> repository, IMapper mapper)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
|
public async Task<IEnumerable<DocReceiverElementDto>> Handle(ReadDocReceiverElementQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var q = _repository.Query;
|
||||||
|
|
||||||
|
if(request.Envelope.Id is int envelopeId)
|
||||||
|
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
|
||||||
|
|
||||||
|
if (request.Envelope.Uuid is string envelopeUuid)
|
||||||
|
q = q.Where(e => e.Document.Envelope.Uuid == envelopeUuid);
|
||||||
|
|
||||||
|
if (request.Receiver.Id is int receiverId)
|
||||||
|
q = q.Where(e => e.ReceiverId == receiverId);
|
||||||
|
|
||||||
|
if (request.Receiver.Signature is string signature)
|
||||||
|
q = q.Where(e => e.Receiver.Signature == signature);
|
||||||
|
|
||||||
|
var elements = await q.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
||||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
||||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
|
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||||
|
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,10 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
|||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations);
|
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
|
||||||
|
|
||||||
|
if (query.UserId is int userId)
|
||||||
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
if (query.Id is not null)
|
if (query.Id is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public record CreateEnvelopeReceiverCommand : CreateEnvelopeCommand, IRequest<Cr
|
|||||||
/// <param name="X">X-Position</param>
|
/// <param name="X">X-Position</param>
|
||||||
/// <param name="Y">Y-Position</param>
|
/// <param name="Y">Y-Position</param>
|
||||||
/// <param name="Page">Seite, auf der sie sich befindet</param>
|
/// <param name="Page">Seite, auf der sie sich befindet</param>
|
||||||
public record Signature([Required] double X, [Required] double Y, [Required] int Page);
|
public record DocReceiverElementCreateDto([Required] double X, [Required] double Y, [Required] int Page);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
|
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
|
||||||
@@ -41,7 +41,7 @@ public class ReceiverGetOrCreateCommand
|
|||||||
/// Unterschriften auf Dokumenten.
|
/// Unterschriften auf Dokumenten.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public List<Signature> Signatures { get; init; } = new();
|
public List<DocReceiverElementCreateDto> DocReceiverElements { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Name, mit dem der Empfänger angesprochen werden soll.
|
/// Der Name, mit dem der Empfänger angesprochen werden soll.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
|
||||||
|
|
||||||
|
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MinLength(1)]
|
||||||
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
internal int UserId { get; private set; }
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommand WithAuth(int userId)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Envelope> _envelopeRepository;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommandHandler(
|
||||||
|
IRepository<Envelope> envelopeRepository,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_envelopeRepository = envelopeRepository;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var reason = request.Reason?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
throw new BadRequestException("Please provide a reason.");
|
||||||
|
|
||||||
|
var envelope = await _envelopeRepository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.Include(e => e.EnvelopeReceivers)
|
||||||
|
.ThenInclude(er => er.Receiver)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
||||||
|
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
||||||
|
|
||||||
|
if (envelope.UserId != request.UserId)
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
||||||
|
|
||||||
|
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
||||||
|
? EnvelopeStatus.EnvelopeWithdrawn
|
||||||
|
: EnvelopeStatus.EnvelopeDeleted;
|
||||||
|
|
||||||
|
envelope.Status = targetStatus;
|
||||||
|
envelope.Comment = reason;
|
||||||
|
envelope.ChangedWhen = DateTime.Now;
|
||||||
|
|
||||||
|
await _envelopeRepository.UpdateAsync(envelope, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
var userReference = envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(userReference))
|
||||||
|
throw new BadRequestException("Envelope sender email is missing.");
|
||||||
|
|
||||||
|
await _mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = userReference,
|
||||||
|
Status = targetStatus,
|
||||||
|
Comment = reason
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||||
|
{
|
||||||
|
if (envelopeReceiver.Receiver is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await _mediator.Publish(new EnvelopeDeletedNotification
|
||||||
|
{
|
||||||
|
Envelope = envelope,
|
||||||
|
EnvelopeReceiver = envelopeReceiver,
|
||||||
|
Reason = reason,
|
||||||
|
SenderReference = userReference
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyActive { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyCompleted { get; init; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage des Include des Umschlags
|
/// Abfrage des Include des Umschlags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -22,7 +32,7 @@ public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<Envelo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? UserId { get; init; }
|
internal int? UserId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Setzt den Benutzerkontext für die Abfrage.
|
/// Setzt den Benutzerkontext für die Abfrage.
|
||||||
@@ -132,8 +142,14 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(request is { OnlyActive: true })
|
||||||
|
query = query.Where(e => Status.Active.Contains(e.Status));
|
||||||
|
|
||||||
|
if (request is { OnlyCompleted: true })
|
||||||
|
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
.Include(e => e.Documents)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a receiver's information.
|
/// Data Transfer Object for updating a receiver's information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class UpdateReceiverCommand
|
public class UpdateReceiverCommand
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
|
|
||||||
@@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
/// um spezifische Informationen über einen Empfänger abzurufen.
|
/// um spezifische Informationen über einen Empfänger abzurufen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
|
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers
|
||||||
|
/// </summary>
|
||||||
|
public virtual string? EmailAddressSearch { 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="ReceiverQueryBase.Id"/>, <see cref="ReceiverQueryBase.EmailAddress"/>, <see cref="EmailAddressSearch"/>, or <see cref="ReceiverQueryBase.Signature"/> is not null.
|
||||||
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[NotMapped]
|
||||||
|
public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
|
|||||||
query = query.Where(r => r.EmailAddress == email);
|
query = query.Where(r => r.EmailAddress == email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch))
|
||||||
|
{
|
||||||
|
query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%"));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Signature is string signature)
|
if (request.Signature is string signature)
|
||||||
{
|
{
|
||||||
query = query.Where(r => r.Signature == signature);
|
query = query.Where(r => r.Signature == signature);
|
||||||
|
|||||||
@@ -397,4 +397,412 @@ public static class Extensions
|
|||||||
/// <param name="suffix"></param>
|
/// <param name="suffix"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
||||||
|
|
||||||
|
// Sender-side UI resources
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
|
||||||
}
|
}
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Bestätigungen</value>
|
<value>Bestätigungen</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Neuer Umschlag</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag laden</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag zurückrufen/löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Daten Aktualisieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Aktualisiert: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Dokument anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger kontaktieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Umschlag-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Öffne Log Verzeichnis</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Ergebnisbericht anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Einladung manuell versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Anrede</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Unterschrieben wann</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Benutzer</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Typ</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titel</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Erstellt am</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Zuletzt geändert am</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Offene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Abgeschlossene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Faktor Eigenschaften</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Telefonnummer</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Datei hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Dateien zusammenführen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Datei löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Datei anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Felder bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Daten bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Speichern</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Abbrechen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Signatur hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Signatur löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Sprache</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode verwenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Faktor-Authentifizierung aktiviert</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Zertifizierungstyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Ersteller</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Erinnerungs-E-Mails senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Erste Erinnerung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Erinnerungsintervall (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Läuft ab nach (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Ablaufwarnung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Nachricht</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Umschlagtyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Alle Optionen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Grund für Löschung</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Bitte geben Sie einen Grund an</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>New Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Load Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Delete Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Reload Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Refreshed: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Show Document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contact Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Envelope-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Open Log Directory</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Show Results Report</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Send Invitation Again</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Salutation</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signed When</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>User</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Title</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Created On</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Last Modified</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Open Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Completed Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Send Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Factor Properties</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Phone Number</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Add Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Delete Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Add File</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Merge Files</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Delete File</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Show File</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Edit Fields</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Edit Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Save</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Send Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Cancel</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Add Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Delete Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Use Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Factor Authentication Enabled</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Certification Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Final Email to Creator</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Final Email to Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Send Reminder Emails</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>First Reminder (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Reminder Interval (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expires After (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Expiry Warning (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Envelope Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>All Options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Deletion Reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Please provide a reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Nouvelle enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Charger l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Supprimer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Actualiser les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Actualisé : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Afficher le document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contacter le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>ID d'enveloppe : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Ouvrir le répertoire des logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Afficher le rapport de résultats</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>E-mail de support</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Renvoyer l'invitation</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Exporter</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Formule de politesse</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signé quand</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Utilisateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titre</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Créé le</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Dernière modification</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes ouvertes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes terminées</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Envoyer le code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>Propriétés 2-facteurs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Nom</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Numéro de téléphone</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Ajouter un destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Supprimer le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Ajouter un fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Fusionner les fichiers</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Supprimer le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Afficher le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Modifier les champs</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Modifier les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Enregistrer</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Envoyer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Annuler</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Ajouter une signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Supprimer la signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Langue</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Utiliser un code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>Authentification à 2 facteurs activée</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Type de certification</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>E-mail final au créateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>E-mail final aux destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Envoyer des e-mails de rappel</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Premier rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Intervalle de rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expire après (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Avertissement d'expiration (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Type d'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Toutes les options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Motif de suppression</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Veuillez indiquer une raison</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Statut</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using DigitalData.Core.Application;
|
using DigitalData.Core.Application;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Services;
|
namespace EnvelopeGenerator.Application.Services;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ namespace EnvelopeGenerator.Application.Services;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, SignatureDto, Signature, int>, IDocumentReceiverElementService
|
public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, DocReceiverElementDto, DocReceiverElement, int>, IDocumentReceiverElementService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
Imports System.IO
|
Imports System.IO
|
||||||
Imports DigitalData.Modules.Database
|
Imports DigitalData.Modules.Database
|
||||||
Imports DigitalData.Modules.Logging
|
Imports DigitalData.Modules.Logging
|
||||||
|
Imports EnvelopeGenerator.CommonServices
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs
|
Imports EnvelopeGenerator.CommonServices.Jobs
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||||
Imports GdPicture14
|
Imports GdPicture14
|
||||||
@@ -145,26 +146,24 @@ Public Class frmFinalizePDF
|
|||||||
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
||||||
|
|
||||||
' 3. Check what BurnAnnotsToPDF will do internally
|
' 3. Check what BurnAnnotsToPDF will do internally
|
||||||
|
Dim oEnvelopeEntity As Envelope = Nothing
|
||||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||||
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
||||||
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
oEnvelopeEntity = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
||||||
|
|
||||||
If envelope Is Nothing Then
|
If oEnvelopeEntity Is Nothing Then
|
||||||
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
|
log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}")
|
||||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
|
log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}")
|
||||||
|
|
||||||
If envelope.ReadOnly Then
|
If oEnvelopeEntity.ReadOnly Then
|
||||||
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
|
log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
|
||||||
txtResult.Text = log.ToString()
|
Else
|
||||||
Return
|
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||||
End If
|
|
||||||
|
|
||||||
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
|
|
||||||
Dim elements = sigRepo _
|
Dim elements = sigRepo _
|
||||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||||
.Include(Function(sig) sig.Annotations) _
|
.Include(Function(sig) sig.Annotations) _
|
||||||
@@ -186,23 +185,52 @@ Public Class frmFinalizePDF
|
|||||||
Else
|
Else
|
||||||
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
||||||
End If
|
End If
|
||||||
|
End If
|
||||||
End Using
|
End Using
|
||||||
|
|
||||||
' 4. Actually call BurnAnnotsToPDF (same as Service)
|
' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged)
|
||||||
log.AppendLine("")
|
log.AppendLine("")
|
||||||
log.AppendLine("Calling BurnAnnotsToPDF...")
|
log.AppendLine("Calling BurnAnnotsToPDF...")
|
||||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||||
log.AppendLine($"Result bytes: {oNewBuffer.Length}")
|
log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}")
|
||||||
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
|
log.AppendLine($"Unchanged (ReadOnly): {oBuffer.Length = oBurnedBuffer.Length AndAlso oBuffer.SequenceEqual(oBurnedBuffer)}")
|
||||||
|
|
||||||
' 5. Write output
|
' 5. Build State and load Envelope domain model for ReportCreator
|
||||||
|
log.AppendLine("")
|
||||||
|
log.AppendLine("Loading envelope domain model for report...")
|
||||||
|
Dim oState As New State() With {
|
||||||
|
.Database = Database,
|
||||||
|
.LogConfig = LogConfig
|
||||||
|
}
|
||||||
|
Dim oEnvelopeModel As New EnvelopeModel(oState)
|
||||||
|
Dim oEnvelope = oEnvelopeModel.GetById(envelopeId)
|
||||||
|
If oEnvelope Is Nothing Then
|
||||||
|
log.AppendLine("ERROR: Envelope not found via EnvelopeModel!")
|
||||||
|
txtResult.Text = log.ToString()
|
||||||
|
Return
|
||||||
|
End If
|
||||||
|
log.AppendLine($"Envelope loaded: {oEnvelope.Id}")
|
||||||
|
|
||||||
|
' 6. Create signing certificate report
|
||||||
|
log.AppendLine("Creating signing certificate report...")
|
||||||
|
Dim oReportCreator As New ReportCreator(LogConfig, oState)
|
||||||
|
Dim oReportBuffer As Byte() = oReportCreator.CreateReport(oEnvelope)
|
||||||
|
log.AppendLine($"Report bytes: {oReportBuffer.Length}")
|
||||||
|
|
||||||
|
' 7. Merge burned document + report (last page)
|
||||||
|
log.AppendLine("Merging documents...")
|
||||||
|
Dim oMerger As New PDFMerger(LogConfig, pGDPictureLicenseKey)
|
||||||
|
Dim oMergedBuffer As Byte() = oMerger.MergeDocuments(oBurnedBuffer, oReportBuffer)
|
||||||
|
log.AppendLine($"Merged bytes: {oMergedBuffer.Length}")
|
||||||
|
|
||||||
|
' 8. Save and open
|
||||||
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
||||||
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
|
Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf")
|
||||||
File.WriteAllBytes(oNewPath, oNewBuffer)
|
File.WriteAllBytes(oMergedPath, oMergedBuffer)
|
||||||
log.AppendLine($"Output: {oNewPath}")
|
log.AppendLine($"Output: {oMergedPath}")
|
||||||
|
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Process.Start(oNewPath)
|
Process.Start(oMergedPath)
|
||||||
|
|
||||||
Catch ex As Exception
|
Catch ex As Exception
|
||||||
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Return pSourceBuffer
|
Return pSourceBuffer
|
||||||
End If
|
End If
|
||||||
|
|
||||||
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
|
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||||
Dim elements = sigRepo _
|
Dim elements = sigRepo _
|
||||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||||
.Include(Function(sig) sig.Annotations) _
|
.Include(Function(sig) sig.Annotations) _
|
||||||
@@ -58,7 +58,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
End Using
|
End Using
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of Signature)) As Byte()
|
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of DocReceiverElement)) As Byte()
|
||||||
' Add background
|
' Add background
|
||||||
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
|
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
|
||||||
'TODO: take the length from the largest y
|
'TODO: take the length from the largest y
|
||||||
@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim y = frameY + yOffsetofFF
|
Dim y = frameY + yOffsetofFF
|
||||||
|
|
||||||
If annot.Type = AnnotationType.FormField Then
|
If annot.Type = AnnotationType.FormField Then
|
||||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
|
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name)
|
||||||
ElseIf annot.Type = AnnotationType.Image Then
|
ElseIf annot.Type = AnnotationType.Image Then
|
||||||
AddImageAnnotation(
|
AddImageAnnotation(
|
||||||
annot.X / inchFactor,
|
annot.X / inchFactor,
|
||||||
@@ -269,11 +269,14 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Next
|
Next
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
|
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String, Optional egName As String = "")
|
||||||
Manager.SelectPage(page)
|
Manager.SelectPage(page)
|
||||||
|
|
||||||
|
' Truncate if needed
|
||||||
|
Dim displayValue As String = TruncateText(value, egName)
|
||||||
|
|
||||||
' Add the text annotation
|
' Add the text annotation
|
||||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
|
Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -294,8 +297,9 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim oHeight = oBounds.Item(3)
|
Dim oHeight = oBounds.Item(3)
|
||||||
|
|
||||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||||
' Add the text annotation
|
' Add the text annotation (truncate if needed)
|
||||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
|
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||||
|
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -306,6 +310,48 @@ Namespace Jobs.FinalizeDocument
|
|||||||
#End Region
|
#End Region
|
||||||
|
|
||||||
#Region "Helpers"
|
#Region "Helpers"
|
||||||
|
''' <summary>
|
||||||
|
''' Truncates a field value so its rendered width fits within the configured max width (inches).
|
||||||
|
''' Uses Graphics.MeasureString with the configured font for pixel-accurate measurement.
|
||||||
|
''' Appends TruncationSuffix when the value is cut.
|
||||||
|
''' </summary>
|
||||||
|
Private Function TruncateText(value As String, fieldEgName As String) As String
|
||||||
|
If String.IsNullOrEmpty(value) Then Return value
|
||||||
|
If _pdfBurnerParams.TextMaxWidths Is Nothing Then Return value
|
||||||
|
|
||||||
|
Dim maxWidthInches As Single = 0
|
||||||
|
If Not _pdfBurnerParams.TextMaxWidths.TryGetValue(fieldEgName, maxWidthInches) Then Return value
|
||||||
|
If maxWidthInches <= 0 Then Return value
|
||||||
|
|
||||||
|
Dim suffix As String = If(_pdfBurnerParams.TruncationSuffix, "")
|
||||||
|
|
||||||
|
Using bmp As New Bitmap(1, 1)
|
||||||
|
Using g As Graphics = Graphics.FromImage(bmp)
|
||||||
|
Dim font As New Font(_pdfBurnerParams.FontName, _pdfBurnerParams.FontSize, _pdfBurnerParams.FontStyle, GraphicsUnit.Point)
|
||||||
|
Dim maxWidthPx As Single = maxWidthInches * g.DpiX
|
||||||
|
|
||||||
|
' Already fits — no truncation needed
|
||||||
|
If g.MeasureString(value, font).Width <= maxWidthPx Then Return value
|
||||||
|
|
||||||
|
' Binary search for the longest prefix that fits (with suffix)
|
||||||
|
Dim lo As Integer = 0
|
||||||
|
Dim hi As Integer = value.Length
|
||||||
|
While lo < hi
|
||||||
|
Dim mid As Integer = (lo + hi + 1) \ 2
|
||||||
|
Dim candidate As String = value.Substring(0, mid) & suffix
|
||||||
|
If g.MeasureString(candidate, font).Width <= maxWidthPx Then
|
||||||
|
lo = mid
|
||||||
|
Else
|
||||||
|
hi = mid - 1
|
||||||
|
End If
|
||||||
|
End While
|
||||||
|
|
||||||
|
Return value.Substring(0, lo) & suffix
|
||||||
|
End Using
|
||||||
|
End Using
|
||||||
|
End Function
|
||||||
|
|
||||||
|
|
||||||
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
||||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||||
|
|||||||
@@ -12,5 +12,24 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Public Property FontSize As Integer = 8
|
Public Property FontSize As Integer = 8
|
||||||
|
|
||||||
Public Property FontStyle As FontStyle = FontStyle.Italic
|
Public Property FontStyle As FontStyle = FontStyle.Italic
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' The suffix appended to a truncated field value. Defaults to "..".
|
||||||
|
''' Can be changed to any string (e.g. "…", "...", "").
|
||||||
|
''' </summary>
|
||||||
|
Public Property TruncationSuffix As String = ".."
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Per-field maximum render width in inches.
|
||||||
|
''' Text is measured using the configured Font/FontSize via Graphics.MeasureString (pixel-accurate).
|
||||||
|
''' If the rendered width exceeds the limit, the text is cut and TruncationSuffix is appended.
|
||||||
|
''' Keys must match egName values used in PDFBurner (e.g. "name", "position", "city", "date").
|
||||||
|
''' Set a value to 0 to disable truncation for that field.
|
||||||
|
''' </summary>
|
||||||
|
Public Property TextMaxWidths As New Dictionary(Of String, Single) From {
|
||||||
|
{"position", 1.41F},
|
||||||
|
{"city", 1.41F},
|
||||||
|
{"date", 2.0F}
|
||||||
|
}
|
||||||
End Class
|
End Class
|
||||||
End Namespace
|
End Namespace
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
Imports EnvelopeGenerator.Domain.Constants
|
Imports EnvelopeGenerator.Domain.Constants
|
||||||
Imports EnvelopeGenerator.Domain.Entities
|
Imports EnvelopeGenerator.Domain.Entities
|
||||||
|
Imports EnvelopeGenerator.Domain.Interfaces
|
||||||
|
|
||||||
Public Class ReportItem
|
Public Class ReportItem
|
||||||
|
|
||||||
@@ -11,8 +12,15 @@ Public Class ReportItem
|
|||||||
Public Property ItemStatus As EnvelopeStatus
|
Public Property ItemStatus As EnvelopeStatus
|
||||||
Public ReadOnly Property ItemStatusTranslated As String
|
Public ReadOnly Property ItemStatusTranslated As String
|
||||||
Get
|
Get
|
||||||
Dim oStatus = ItemStatus.ToString()
|
Dim oStatusKey = ItemStatus.ToString()
|
||||||
Return My.Resources.Model.ResourceManager.GetString(oStatus)
|
|
||||||
|
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
|
||||||
|
Envelope IsNot Nothing AndAlso
|
||||||
|
Envelope.IsReadAndConfirm() Then
|
||||||
|
oStatusKey = "DocumentSignedRaC"
|
||||||
|
End If
|
||||||
|
|
||||||
|
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ Public Class ElementModel
|
|||||||
MyBase.New(pState)
|
MyBase.New(pState)
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Function ToElement(pRow As DataRow) As Signature
|
Private Function ToElement(pRow As DataRow) As DocReceiverElement
|
||||||
Return New Signature() With {
|
Return New DocReceiverElement() With {
|
||||||
.Id = pRow.ItemEx("GUID", 0),
|
.Id = pRow.ItemEx("GUID", 0),
|
||||||
.DocumentId = pRow.ItemEx("DOCUMENT_ID", 0),
|
.DocumentId = pRow.ItemEx("DOCUMENT_ID", 0),
|
||||||
.ReceiverId = pRow.ItemEx("RECEIVER_ID", 0),
|
.ReceiverId = pRow.ItemEx("RECEIVER_ID", 0),
|
||||||
@@ -24,7 +24,7 @@ Public Class ElementModel
|
|||||||
}
|
}
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function ToElements(pTable As DataTable) As List(Of Signature)
|
Private Function ToElements(pTable As DataTable) As List(Of DocReceiverElement)
|
||||||
Return pTable?.Rows.Cast(Of DataRow).
|
Return pTable?.Rows.Cast(Of DataRow).
|
||||||
Select(AddressOf ToElement).
|
Select(AddressOf ToElement).
|
||||||
ToList()
|
ToList()
|
||||||
@@ -80,7 +80,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function List(pDocumentId As Integer) As List(Of Signature)
|
Public Function List(pDocumentId As Integer) As List(Of DocReceiverElement)
|
||||||
Try
|
Try
|
||||||
Dim oSql = $"SELECT * FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE DOCUMENT_ID = {pDocumentId} ORDER BY PAGE ASC"
|
Dim oSql = $"SELECT * FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE DOCUMENT_ID = {pDocumentId} ORDER BY PAGE ASC"
|
||||||
Dim oTable = Database.GetDatatable(oSql)
|
Dim oTable = Database.GetDatatable(oSql)
|
||||||
@@ -93,7 +93,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function List(pDocumentId As Integer, pReceiverId As Integer) As List(Of Signature)
|
Public Function List(pDocumentId As Integer, pReceiverId As Integer) As List(Of DocReceiverElement)
|
||||||
Try
|
Try
|
||||||
Dim oReceiverConstraint = ""
|
Dim oReceiverConstraint = ""
|
||||||
If pReceiverId > 0 Then
|
If pReceiverId > 0 Then
|
||||||
@@ -111,7 +111,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function Insert(pElement As Signature) As Boolean
|
Public Function Insert(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
Dim oSql = "INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
Dim oSql = "INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
||||||
([DOCUMENT_ID]
|
([DOCUMENT_ID]
|
||||||
@@ -161,7 +161,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function Update(pElement As Signature) As Boolean
|
Public Function Update(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
Dim oSql = "UPDATE [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
Dim oSql = "UPDATE [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
||||||
SET [POSITION_X] = @POSITION_X
|
SET [POSITION_X] = @POSITION_X
|
||||||
@@ -185,7 +185,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function GetElementId(pElement As Signature) As Integer
|
Private Function GetElementId(pElement As DocReceiverElement) As Integer
|
||||||
Try
|
Try
|
||||||
Return Database.GetScalarValue($"SELECT MAX(GUID) FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT
|
Return Database.GetScalarValue($"SELECT MAX(GUID) FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT
|
||||||
WHERE DOCUMENT_ID = {pElement.DocumentId} AND RECEIVER_ID = {pElement.ReceiverId}")
|
WHERE DOCUMENT_ID = {pElement.DocumentId} AND RECEIVER_ID = {pElement.ReceiverId}")
|
||||||
@@ -196,7 +196,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function DeleteElement(pElement As Signature) As Boolean
|
Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
Dim oSql = $"DELETE FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT WHERE GUID = {pElement.Id}"
|
Dim oSql = $"DELETE FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT WHERE GUID = {pElement.Id}"
|
||||||
Return Database.ExecuteNonQuery(oSql)
|
Return Database.ExecuteNonQuery(oSql)
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Document signed</value>
|
<value>Document signed</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Read and confirmed</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Draft</value>
|
<value>Draft</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
{
|
{
|
||||||
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/
|
// http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/
|
||||||
public enum EnvelopeStatus
|
public enum EnvelopeStatus
|
||||||
{
|
{
|
||||||
Invalid = 0,
|
Invalid = 0,
|
||||||
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
EnvelopeStatus.EnvelopeCreated,
|
EnvelopeStatus.EnvelopeCreated,
|
||||||
EnvelopeStatus.DocumentMod_Rotation
|
EnvelopeStatus.DocumentMod_Rotation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsActive())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsCompleted())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EnvelopeStatusExtensions
|
||||||
|
{
|
||||||
|
public static bool IsActive(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsCompleted(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
@@ -15,7 +16,11 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
public static class Receiver
|
public static class Receiver
|
||||||
{
|
{
|
||||||
public const string TFA = "EGReceiverTFA";
|
public const string TFA = "EGReceiverTFA";
|
||||||
|
|
||||||
|
[Obsolete("Use Receiver.Authorized")]
|
||||||
public const string Full = "EGReceiver";
|
public const string Full = "EGReceiver";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<string> Authorized = new[] { "receiver", "EGReceiver" };
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string Sender = "EGSender";
|
public const string Sender = "EGSender";
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ using System.Collections.Generic;
|
|||||||
namespace EnvelopeGenerator.Domain.Entities
|
namespace EnvelopeGenerator.Domain.Entities
|
||||||
{
|
{
|
||||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
||||||
public class Signature : ISignature, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
public class DocReceiverElement : IDocReceiverElement, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
||||||
{
|
{
|
||||||
public Signature()
|
public DocReceiverElement()
|
||||||
{
|
{
|
||||||
// TODO: * Check the Form App and remove the default value
|
// TODO: * Check the Form App and remove the default value
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
@@ -126,5 +126,33 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double Left => Math.Round(X, 5);
|
public double Left => Math.Round(X, 5);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
[Column("FULL_NAME", TypeName = "nvarchar(100)")]
|
||||||
|
public string
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
# endif
|
||||||
|
FullName { get; set; }
|
||||||
|
|
||||||
|
[Column("POSITION", TypeName = "nvarchar(100)")]
|
||||||
|
public string
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
Position { get; set; }
|
||||||
|
|
||||||
|
[Column("PLACE", TypeName = "nvarchar(100)")]
|
||||||
|
public string
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
Place { get; set; }
|
||||||
|
|
||||||
|
[Column("INK", TypeName = "varbinary(MAX)")]
|
||||||
|
public byte[]
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
Ink { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public Document()
|
public Document()
|
||||||
{
|
{
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
Elements = Enumerable.Empty<Signature>().ToList();
|
Elements = Enumerable.Empty<DocReceiverElement>().ToList();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
FileNameOriginal { get; set; }
|
FileNameOriginal { get; set; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public virtual List<Signature>
|
public virtual List<DocReceiverElement>
|
||||||
#if nullable
|
#if nullable
|
||||||
?
|
?
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
ChangedWho { get; set; }
|
ChangedWho { get; set; }
|
||||||
|
|
||||||
[ForeignKey("ElementId")]
|
[ForeignKey("ElementId")]
|
||||||
public virtual Signature
|
public virtual DocReceiverElement
|
||||||
#if nullable
|
#if nullable
|
||||||
?
|
?
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public string Language { get; set; }
|
public string Language { get; set; }
|
||||||
|
|
||||||
[Column("SEND_REMINDER_EMAILS")]
|
[Column("SEND_REMINDER_EMAILS")]
|
||||||
public bool SendReminderEmails { get; set; }
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
[Column("FIRST_REMINDER_DAYS")]
|
[Column("FIRST_REMINDER_DAYS")]
|
||||||
public int? FirstReminderDays { get; set; }
|
public int? FirstReminderDays { get; set; }
|
||||||
@@ -114,7 +114,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public int? CertificationType { get; set; }
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
[Column("USE_ACCESS_CODE")]
|
[Column("USE_ACCESS_CODE")]
|
||||||
public bool UseAccessCode { get; set; }
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_CREATOR")]
|
[Column("FINAL_EMAIL_TO_CREATOR")]
|
||||||
public int? FinalEmailToCreator { get; set; }
|
public int? FinalEmailToCreator { get; set; }
|
||||||
@@ -132,7 +132,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
[Column("TFA_ENABLED")]
|
[Column("TFA_ENABLED")]
|
||||||
public bool TfaEnabled { get; set; }
|
public bool? TfaEnabled { get; set; }
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
= false;
|
= false;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Domain.Interfaces
|
namespace EnvelopeGenerator.Domain.Interfaces
|
||||||
{
|
{
|
||||||
public interface ISignature
|
public interface IDocReceiverElement
|
||||||
{
|
{
|
||||||
int Page { get; set; }
|
int Page { get; set; }
|
||||||
|
|
||||||
@@ -213,6 +213,15 @@ namespace My.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Gelesen und bestätigt.
|
||||||
|
/// </summary>
|
||||||
|
public static string DocumentSignedRaC {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Entwurf.
|
/// Looks up a localized string similar to Entwurf.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Public Class FieldEditorController
|
|||||||
Inherits BaseController
|
Inherits BaseController
|
||||||
|
|
||||||
Private ReadOnly Document As Document
|
Private ReadOnly Document As Document
|
||||||
Public Property Elements As New List(Of Signature)
|
Public Property Elements As New List(Of DocReceiverElement)
|
||||||
|
|
||||||
Public Class ElementInfo
|
Public Class ElementInfo
|
||||||
Public ReceiverId As Integer
|
Public ReceiverId As Integer
|
||||||
@@ -36,7 +36,7 @@ Public Class FieldEditorController
|
|||||||
}
|
}
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function GetElementByPosition(pAnnotation As AnnotationStickyNote, pPage As Integer, pReceiverId As Integer) As Signature
|
Private Function GetElementByPosition(pAnnotation As AnnotationStickyNote, pPage As Integer, pReceiverId As Integer) As DocReceiverElement
|
||||||
Dim oElement = Elements.
|
Dim oElement = Elements.
|
||||||
Where(Function(e)
|
Where(Function(e)
|
||||||
Return e.Left = CSng(Math.Round(pAnnotation.Left, 5)) And
|
Return e.Left = CSng(Math.Round(pAnnotation.Left, 5)) And
|
||||||
@@ -47,12 +47,12 @@ Public Class FieldEditorController
|
|||||||
Return oElement
|
Return oElement
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function GetElementByGuid(pGuid As Integer) As Signature
|
Private Function GetElementByGuid(pGuid As Integer) As DocReceiverElement
|
||||||
Dim oElement = Elements.Where(Function(e) pGuid = e.Id).SingleOrDefault()
|
Dim oElement = Elements.Where(Function(e) pGuid = e.Id).SingleOrDefault()
|
||||||
Return oElement
|
Return oElement
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function GetElement(pAnnotation As AnnotationStickyNote) As Signature
|
Public Function GetElement(pAnnotation As AnnotationStickyNote) As DocReceiverElement
|
||||||
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
||||||
|
|
||||||
If oInfo.Guid = -1 Then
|
If oInfo.Guid = -1 Then
|
||||||
@@ -85,7 +85,7 @@ Public Class FieldEditorController
|
|||||||
Else
|
Else
|
||||||
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
||||||
|
|
||||||
Elements.Add(New Signature() With {
|
Elements.Add(New DocReceiverElement() With {
|
||||||
.ElementType = Constants.ElementType.Signature,
|
.ElementType = Constants.ElementType.Signature,
|
||||||
.Height = oAnnotationHeight,
|
.Height = oAnnotationHeight,
|
||||||
.Width = oAnnotationWidth,
|
.Width = oAnnotationWidth,
|
||||||
@@ -98,7 +98,7 @@ Public Class FieldEditorController
|
|||||||
End If
|
End If
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Public Function ClearElements(pPage As Integer, pReceiverId As Integer) As IEnumerable(Of Signature)
|
Public Function ClearElements(pPage As Integer, pReceiverId As Integer) As IEnumerable(Of DocReceiverElement)
|
||||||
Return Elements.
|
Return Elements.
|
||||||
Where(Function(e) e.Page <> pPage And e.ReceiverId <> pReceiverId).
|
Where(Function(e) e.Page <> pPage And e.ReceiverId <> pReceiverId).
|
||||||
ToList()
|
ToList()
|
||||||
@@ -120,7 +120,7 @@ Public Class FieldEditorController
|
|||||||
All(Function(pResult) pResult = True)
|
All(Function(pResult) pResult = True)
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function SaveElement(pElement As Signature) As Boolean
|
Public Function SaveElement(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
If pElement.Id > 0 Then
|
If pElement.Id > 0 Then
|
||||||
Return ElementModel.Update(pElement)
|
Return ElementModel.Update(pElement)
|
||||||
@@ -134,11 +134,11 @@ Public Class FieldEditorController
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function DeleteElement(pElement As Signature) As Boolean
|
Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
' Element aus Datenbank löschen
|
' Element aus Datenbank löschen
|
||||||
If ElementModel.DeleteElement(pElement) Then
|
If ElementModel.DeleteElement(pElement) Then
|
||||||
Dim oElement = New List(Of Signature)() From {pElement}
|
Dim oElement = New List(Of DocReceiverElement)() From {pElement}
|
||||||
Elements = Elements.Except(oElement).ToList()
|
Elements = Elements.Except(oElement).ToList()
|
||||||
Return True
|
Return True
|
||||||
Else
|
Else
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ Partial Public Class frmFieldEditor
|
|||||||
End If
|
End If
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub LoadAnnotation(pElement As Signature, pReceiverId As Integer)
|
Private Sub LoadAnnotation(pElement As DocReceiverElement, pReceiverId As Integer)
|
||||||
Dim oAnnotation As AnnotationStickyNote = Manager.AddStickyNoteAnnot(0, 0, 0, 0, "SIGNATUR")
|
Dim oAnnotation As AnnotationStickyNote = Manager.AddStickyNoteAnnot(0, 0, 0, 0, "SIGNATUR")
|
||||||
Dim oPage = pElement.Page
|
Dim oPage = pElement.Page
|
||||||
Dim oReceiver = Receivers.Where(Function(r) r.Id = pReceiverId).Single()
|
Dim oReceiver = Receivers.Where(Function(r) r.Id = pReceiverId).Single()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using EnvelopeGenerator.Infrastructure.Repositories;
|
|||||||
using EnvelopeGenerator.Infrastructure.Executor;
|
using EnvelopeGenerator.Infrastructure.Executor;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
#elif NETFRAMEWORK
|
#elif NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
@@ -55,6 +56,9 @@ namespace EnvelopeGenerator.Infrastructure
|
|||||||
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
||||||
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
||||||
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
||||||
|
|
||||||
|
// User Preferences — distributed cache (dbo.TBDD_CACHE)
|
||||||
|
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
services.AddDbRepository(opt =>
|
services.AddDbRepository(opt =>
|
||||||
@@ -74,14 +78,14 @@ namespace EnvelopeGenerator.Infrastructure
|
|||||||
services.AddSQLExecutor<Envelope>();
|
services.AddSQLExecutor<Envelope>();
|
||||||
services.AddSQLExecutor<Receiver>();
|
services.AddSQLExecutor<Receiver>();
|
||||||
services.AddSQLExecutor<Document>();
|
services.AddSQLExecutor<Document>();
|
||||||
services.AddSQLExecutor<Signature>();
|
services.AddSQLExecutor<DocReceiverElement>();
|
||||||
services.AddSQLExecutor<DocumentStatus>();
|
services.AddSQLExecutor<DocumentStatus>();
|
||||||
|
|
||||||
SetDapperTypeMap<Envelope>();
|
SetDapperTypeMap<Envelope>();
|
||||||
SetDapperTypeMap<User>();
|
SetDapperTypeMap<User>();
|
||||||
SetDapperTypeMap<Receiver>();
|
SetDapperTypeMap<Receiver>();
|
||||||
SetDapperTypeMap<Document>();
|
SetDapperTypeMap<Document>();
|
||||||
SetDapperTypeMap<Signature>();
|
SetDapperTypeMap<DocReceiverElement>();
|
||||||
SetDapperTypeMap<DocumentStatus>();
|
SetDapperTypeMap<DocumentStatus>();
|
||||||
|
|
||||||
services.AddScoped<IEnvelopeExecutor, EnvelopeExecutor>();
|
services.AddScoped<IEnvelopeExecutor, EnvelopeExecutor>();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
public DbSet<Envelope> Envelopes { get; set; }
|
public DbSet<Envelope> Envelopes { get; set; }
|
||||||
|
|
||||||
public DbSet<Signature> DocumentReceiverElements { get; set; }
|
public DbSet<DocReceiverElement> DocumentReceiverElements { get; set; }
|
||||||
|
|
||||||
public DbSet<ElementAnnotation> DocumentReceiverElementAnnotations { get; set; }
|
public DbSet<ElementAnnotation> DocumentReceiverElementAnnotations { get; set; }
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
#endregion EnvelopeDocument
|
#endregion EnvelopeDocument
|
||||||
|
|
||||||
#region DocumentReceiverElement
|
#region DocumentReceiverElement
|
||||||
modelBuilder.Entity<Signature>()
|
modelBuilder.Entity<DocReceiverElement>()
|
||||||
.HasOne(dre => dre.Document)
|
.HasOne(dre => dre.Document)
|
||||||
.WithMany(ed => ed.Elements)
|
.WithMany(ed => ed.Elements)
|
||||||
.HasForeignKey(dre => dre.DocumentId);
|
.HasForeignKey(dre => dre.DocumentId);
|
||||||
@@ -196,7 +196,7 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
#endregion DocumentStatus
|
#endregion DocumentStatus
|
||||||
|
|
||||||
#region Annotation
|
#region Annotation
|
||||||
modelBuilder.Entity<Signature>()
|
modelBuilder.Entity<DocReceiverElement>()
|
||||||
.HasMany(signature => signature.Annotations)
|
.HasMany(signature => signature.Annotations)
|
||||||
.WithOne(annot => annot.Element)
|
.WithOne(annot => annot.Element)
|
||||||
.HasForeignKey(annot => annot.ElementId);
|
.HasForeignKey(annot => annot.ElementId);
|
||||||
@@ -217,7 +217,7 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
// TODO: call add trigger methods with attributes and reflection
|
// TODO: call add trigger methods with attributes and reflection
|
||||||
AddTrigger<Config>();
|
AddTrigger<Config>();
|
||||||
AddTrigger<Signature>();
|
AddTrigger<DocReceiverElement>();
|
||||||
AddTrigger<DocumentStatus>();
|
AddTrigger<DocumentStatus>();
|
||||||
AddTrigger<EmailTemplate>();
|
AddTrigger<EmailTemplate>();
|
||||||
AddTrigger<Envelope>();
|
AddTrigger<Envelope>();
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
#if NET
|
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure
|
|
||||||
{
|
|
||||||
public class EGDbContextFactory : IDesignTimeDbContextFactory<EGDbContext>
|
|
||||||
{
|
|
||||||
public EGDbContext CreateDbContext(string[] args)
|
|
||||||
{
|
|
||||||
var config = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
|
||||||
.AddJsonFile("appsettings.migration.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// create DbContextOptions
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<EGDbContext>();
|
|
||||||
optionsBuilder.UseSqlServer(config.GetConnectionString("Default"));
|
|
||||||
|
|
||||||
// create DbTriggerParams
|
|
||||||
var triggerLists = config.GetSection("DbTriggerParams").Get<Dictionary<string, List<string>>>();
|
|
||||||
var dbTriggerParams = new DbTriggerParams();
|
|
||||||
if (triggerLists is not null)
|
|
||||||
foreach (var triggerList in triggerLists)
|
|
||||||
{
|
|
||||||
if (triggerList.Value.Count == 0)
|
|
||||||
continue; // Skip empty trigger lists
|
|
||||||
|
|
||||||
var tableName = triggerList.Key;
|
|
||||||
|
|
||||||
dbTriggerParams[tableName] = new List<string>();
|
|
||||||
|
|
||||||
foreach (var trigger in triggerList.Value)
|
|
||||||
{
|
|
||||||
dbTriggerParams[tableName].Add(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams));
|
|
||||||
dbContext.IsMigration = true;
|
|
||||||
return dbContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user