Compare commits
198 Commits
feat/serve
...
refactor/w
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cf603fc79 | |||
| e3d2225a44 | |||
| 595bbf3f25 | |||
| ee7d01ed7f | |||
| 85104511fd | |||
| 1b87528cca | |||
| 2913514da8 | |||
| ce7e70162b | |||
| 5e00b6e9ec | |||
| f0b4c8d00c | |||
| abf616c115 | |||
| 42585e1f2c | |||
| ff10fc6b0a | |||
| 7222f7e9a7 | |||
| d913b949e0 | |||
| a35e05d016 | |||
| 8aabc3e233 | |||
| 8b7f99eb76 | |||
| 91e90b322b | |||
| 74e55e9206 | |||
| fc5ad7ea3c | |||
| ff5582c9b5 | |||
| 6523cca945 | |||
| 28d0b6f81c | |||
| 5df38754cb | |||
| c2b7579717 | |||
| ae1ecbc15f | |||
| 4dcfe67038 | |||
| c812d4dc1f | |||
| 8d27718062 | |||
| c746612d80 | |||
| 06551d502b | |||
| 0f28b80e16 | |||
| f7af3776c2 | |||
| 8ba9e8ec20 | |||
| 226d852c6f | |||
| fb155fe706 | |||
| 658cb28e1c | |||
| f38098aed8 | |||
| 00c3fd18a9 | |||
| 8929c517fe | |||
| 07ce6ccfe1 | |||
| 26beae0d14 | |||
| e423c6d751 | |||
| 56d1dc050f | |||
| 016dec74f9 | |||
| b9cd629766 | |||
| e729adb441 | |||
| 67d0b07926 | |||
| 780277d656 | |||
| 635d5d8278 | |||
| f1c8e223dd | |||
| 31bf64a6f2 | |||
| 93142a56f7 | |||
| 64baef907c | |||
| 232b341894 | |||
| 57a08e8a89 | |||
| 198eabcbae | |||
| 04cea1ca4b | |||
| 29108594d9 | |||
| 51c0f51738 | |||
| 816bbce9c6 | |||
| 5194d638ac | |||
| 6b3622b366 | |||
| 4f8fa9afd8 | |||
| 7a36316b7e | |||
| 68d9dbdcdf | |||
| 7f3d1afcb9 | |||
| 955ac57a1a | |||
| b8a96330c0 | |||
| 8f2744afc7 | |||
| c8ddfac8d0 | |||
| 1942d78155 | |||
| f361898ba4 | |||
| 7766e2ea25 | |||
| 6dfcc1b2be | |||
| 63c60a91b2 | |||
| a5d2c477f6 | |||
| 3a71d57619 | |||
| 73b6c2712c | |||
| 200972c370 | |||
| 9f7519941a | |||
| 66e58b4a1a | |||
| de9d8c6f37 | |||
| 47f48ba12a | |||
| 60241c9c6d | |||
| 2a939b4cad | |||
| e89d471c6f | |||
| ece676fbcf | |||
| fac4ec28a4 | |||
| a3b11ed63f | |||
| 5627483589 | |||
| bf23e88cf8 | |||
| 9435561e78 | |||
| db1e91a3f6 | |||
| 0c664c0995 | |||
| 5db2f6f1e3 | |||
| a535e20b0e | |||
| bb35a68b1f | |||
| 715add7f51 | |||
| 621e22701a | |||
| 5ef32a2b0c | |||
| 54d16eb3b8 | |||
| ae3264253d | |||
| 4cbfe52d5b | |||
| 27c6b017cd | |||
| 8378c33465 | |||
| c7c4e1194d | |||
| 3b367eff7f | |||
| d0c3d79fbc | |||
| e81c7896a1 | |||
| 127e2b253e | |||
| 6d0b78072e | |||
| 3721ea9a1f | |||
| f86588551c | |||
| 0eaa9ff08f | |||
| ec79d7622e | |||
| f558d03bdf | |||
| db79053c62 | |||
| 85dd85aa92 | |||
| c7bc9fbcca | |||
| 1e8e5c96c4 | |||
| 61d04ca51a | |||
| af674a811d | |||
| 123889451f | |||
| 3b5075fe44 | |||
| 48302a374f | |||
| b2c245d6bd | |||
| e9c0ec139a | |||
| 819bdcfcfb | |||
| ad7686b7c2 | |||
| ade542ded0 | |||
| 01c1283622 | |||
| d969dbe843 | |||
| 41779a4f58 | |||
| bf739c1360 | |||
| 0f167d731e | |||
| 021d7cfe29 | |||
| e2322b41cd | |||
| 618451f0c1 | |||
| 96c7cd6e8a | |||
| 4047ba7c6b | |||
| 8386b85592 | |||
| 640f0b95fe | |||
| 181f7f4dec | |||
| 95961a296f | |||
| db613358d5 | |||
| 7a126050cf | |||
| 7081c5f64e | |||
| 12ec5b28fa | |||
| 925e60a45c | |||
| 897860b2bf | |||
| 8933cff558 | |||
| b0b9ba7212 | |||
| 33c75ea7e1 | |||
| 513efa0833 | |||
| 0c81d79a9b | |||
| 696cd1b623 | |||
| 2434f67627 | |||
| 776700b247 | |||
| 7a114f47a6 | |||
| ed44d841e7 | |||
| 4bd0cc2e94 | |||
| 8a4963472c | |||
| 4d3f72a3ed | |||
| 1ebd43e3b0 | |||
| 19aeda5aef | |||
| 940d129c11 | |||
| 44bbdbb835 | |||
| 223017006b | |||
| ad9250c7b1 | |||
| 5cc47ca9c7 | |||
| 156988403a | |||
| e960c02cd4 | |||
| a0e2492757 | |||
| 28f8b829e3 | |||
| d00c459124 | |||
| 287c14e114 | |||
| 212ba9d569 | |||
| 735f072dd1 | |||
| 48cfd3f416 | |||
| 0d2e43b2d6 | |||
| 5aaa37b8ec | |||
| 051a8ce211 | |||
| 519a8bf3f9 | |||
| a7d0b40e39 | |||
| 3dc9a180d3 | |||
| 4dba965c75 | |||
| 4d2538cc2c | |||
| d201eacc97 | |||
| 9daa671655 | |||
| 3fbf011fe3 | |||
| a815705fa4 | |||
| 6e44d0d4b5 | |||
| 42a853cb0d | |||
| b46c3c8ebe | |||
| 9f712a6b8c | |||
| 4449743105 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -370,3 +370,4 @@ FodyWeavers.xsd
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||
/ignore/EnvelopeGenerator.Form
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json
|
||||
|
||||
142
AGENTS.md
142
AGENTS.md
@@ -48,6 +48,16 @@ This section explains every markdown file in the repo: what it contains, why it
|
||||
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
|
||||
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
|
||||
|
||||
#### `DATABASE_STRUCTURE_CONTEXT.md`
|
||||
**What:** Deep DB documentation for the envelope domain. Includes table/column snapshot, entity-to-table mapping, EF relationship/constraint map, stored procedure behavior + call sites, and trigger behavior + wiring path.
|
||||
**Why it exists:** DB is centralized and can evolve independently; this file preserves migration-critical data semantics when historical DB context is hard to recover.
|
||||
**When you need it:**
|
||||
- Planning or reviewing DB/schema changes
|
||||
- Debugging status transitions/history side effects
|
||||
- Tracing where `PRSIG_*` procedures or DB triggers affect app behavior
|
||||
- Reconciling Domain entities with physical SQL schema
|
||||
**Maintenance:** Refresh on each DB migration, trigger/procedure change, or significant Domain/EGDbContext model change.
|
||||
|
||||
---
|
||||
|
||||
### Deployment Docs
|
||||
@@ -63,15 +73,45 @@ This section explains every markdown file in the repo: what it contains, why it
|
||||
|
||||
---
|
||||
|
||||
## Markdown Audit Snapshot (2026-10-05)
|
||||
|
||||
The following table summarizes repository markdown files by practical status: completed, partially complete, or removable candidates.
|
||||
|
||||
| File | Type | Status | Missing / Gap | Removal Recommendation |
|
||||
|---|---|---|---|---|
|
||||
| `AGENTS.md` | Agent onboarding | Partially complete | Route/migration statements can drift from real code over time. | Keep |
|
||||
| `COPILOT_CONTEXT.md` | Architecture reference | Partially complete | Some route/render details may be out of sync with other docs; needs regular reconciliation. | Keep |
|
||||
| `FORM_APPLICATION_CONTEXT.md` | Legacy-to-modern feature spec | Partially complete | Large sections still marked not implemented / needs implementation. | Keep |
|
||||
| `RECEIVER_PDF_VIEWER_CONTEXT.md` | Receiver PDF viewer deep-dive | Partially complete | Long migration plan needs periodic sync with active implementation. | Keep |
|
||||
| `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` | Sender workflow implementation plan | Partially complete | No explicit implementation progress markers per phase. | Keep (until migration closes) |
|
||||
| `SENDER_PDF_TOOLBAR_PLAN_EN.md` | Sender PDF toolbar plan | Incomplete | Explicit pending item remains (`Toolbar visual polish`). | Keep (until done) |
|
||||
| `SENDER_EDITOR_WASM_PDF_EDITOR_IMPLEMENTATION_PLAN_en.md` | Sender editor WASM migration plan | Incomplete | Plan exists but completion/progress tracking is minimal. | Keep (until done) |
|
||||
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md` | Deployment guide | Complete (operational) | No critical gap identified. | Keep |
|
||||
| `envelope-generator-ui/README.md` | Angular scaffold README | Complete but generic | Template text only; low project-specific value. | Removable or replace with project-specific summary |
|
||||
| `EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Third-party vendor README | Complete (vendor) | Not project workflow documentation. | Keep with vendor assets |
|
||||
| `EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Third-party license | Complete (legal) | No gap. | Keep |
|
||||
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md` | Third-party vendor README | Complete (vendor) | Not project-specific guidance. | Keep with vendor assets |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/README.md` | Legacy PSPDFKit note | Outdated | Mentions deprecated PSPDFKit-based flow; may mislead if reused. | Removable/archive |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Duplicate vendor README | Redundant | Duplicate copy under `ignore/`. | Removable/archive |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/bootstrap-icons/README.md` | Vendor README in archive area | Secondary | Archive-only utility, not active docs. | Removable/archive |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Duplicate license in archive area | Redundant | Duplicate copy under `ignore/`. | Removable/archive (if legal policy allows) |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/README.md` | Duplicate Angular scaffold README | Redundant | Generic template + duplicate in archive path. | Removable/archive |
|
||||
|
||||
### Documentation Audit TODO
|
||||
|
||||
- [ ] Validate this markdown audit table against the current codebase and update statuses (`complete`, `partial`, `incomplete`, `removable`) when implementation changes land.
|
||||
|
||||
---
|
||||
|
||||
## Active Architecture (Post-Migration)
|
||||
|
||||
**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)
|
||||
**Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
|
||||
|
||||
**Proxy:** YARP in EnvelopeGenerator.Server routes `/api/*` → `localhost:8088` (API)
|
||||
**Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
|
||||
|
||||
### Deprecated Projects - DO NOT USE
|
||||
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
||||
@@ -80,18 +120,14 @@ This section explains every markdown file in the repo: what it contains, why it
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Run Both Projects (Required)
|
||||
### Run Active Host
|
||||
```powershell
|
||||
# Terminal 1 - API Backend
|
||||
cd EnvelopeGenerator.API
|
||||
dotnet run
|
||||
|
||||
# Terminal 2 - Blazor Frontend
|
||||
# Terminal - Blazor + Web API host
|
||||
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||
dotnet run
|
||||
```
|
||||
|
||||
**Critical:** Both must run simultaneously. EnvelopeGenerator.Server proxy forwards `/api/*` to API.
|
||||
**Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
|
||||
|
||||
### Build
|
||||
```powershell
|
||||
@@ -104,10 +140,10 @@ dotnet build EnvelopeGenerator.sln
|
||||
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
|
||||
EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
|
||||
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||
├─ Controllers/ # Active Web API endpoints
|
||||
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
||||
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||
├─ Services/ # HTTP API clients
|
||||
@@ -156,15 +192,13 @@ float inches = (pixelX / canvasWidth) * pageWidthInches;
|
||||
- **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)
|
||||
### Sender Workflow Endpoint
|
||||
```
|
||||
POST /api/Envelope/draft # Create draft envelope
|
||||
PUT /api/Envelope/{id} # Update metadata
|
||||
POST /api/Envelope # Create/update/save/send envelope workflow payload
|
||||
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.
|
||||
@@ -199,7 +233,7 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
||||
}
|
||||
```
|
||||
|
||||
### API Config (`API/appsettings.json`)
|
||||
### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
|
||||
- `ConnectionStrings:Default` - SQL Server DB
|
||||
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
||||
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
||||
@@ -237,27 +271,84 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
||||
**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}`
|
||||
1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||
2. Navigate to `https://localhost:5131` (or check console output for port)
|
||||
3. Test sender login at `/sender/login`
|
||||
4. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||
|
||||
## Technical Debt TODOs
|
||||
|
||||
- **CRITICAL (Licensing / pre-commercial release):** Remove all `SixLabors.ImageSharp` and `QRCoder-ImageSharp` package dependencies from production/runtime projects before commercial release. Replace remaining QR/image paths with permissive-license alternatives (no copyleft/share-source obligations), run repo-wide package audit (`*.csproj`), and add CI guardrails to block reintroduction.
|
||||
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
|
||||
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
|
||||
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
|
||||
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
|
||||
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
|
||||
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
|
||||
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
|
||||
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
|
||||
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
|
||||
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
||||
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
||||
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
||||
- Implement SPA-style language switching so the UI culture updates immediately without a full page reload.
|
||||
- Implement sender-scoped caching for "recent receiver suggestions" used during envelope creation (wizard/editor flows): cache key should include sender identity, support short TTL + invalidation hooks after successful envelope receiver writes, and prevent cross-sender suggestion leakage.
|
||||
|
||||
## Database
|
||||
|
||||
**SQL Server** (DD_ECM)
|
||||
- Connection string in `API/appsettings.json`
|
||||
- Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
|
||||
- EF Core migrations NOT used (manual SQL scripts)
|
||||
- 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_DOCUMENT_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
|
||||
- `TBSIG_RECEIVER` - Receiver registry
|
||||
- `TBSIG_DOCUMENT` - PDF binary data
|
||||
- `TBSIG_ENVELOPE_DOCUMENT` - PDF binary data
|
||||
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
||||
|
||||
### Data Structure (Domain + EGDbContext)
|
||||
|
||||
Use this as the canonical quick map before touching sender/receiver workflow code.
|
||||
|
||||
#### Core Aggregate Shape
|
||||
- `Envelope` (`TBSIG_ENVELOPE`) is the root aggregate for sending workflow.
|
||||
- `Envelope -> Documents` is `1:N` via `Document.EnvelopeId` (`TBSIG_ENVELOPE_DOCUMENT`).
|
||||
- `Envelope -> Histories` is `1:N` via `History.EnvelopeId` (`TBSIG_ENVELOPE_HISTORY`).
|
||||
- `Envelope <-> Receiver` is `N:N` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{EnvelopeId, ReceiverId}`).
|
||||
- `Document -> DocReceiverElement` is `1:N` via `DocReceiverElement.DocumentId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT`).
|
||||
- `DocReceiverElement -> ElementAnnotation` is `1:N` via `ElementAnnotation.ElementId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`).
|
||||
|
||||
#### Status and Tracking Entities
|
||||
- `DocumentStatus` (`TBSIG_DOCUMENT_STATUS`) links `EnvelopeId + ReceiverId` and stores receiver-level signing state.
|
||||
- `History` stores timeline events (`EnvelopeStatus`) with polymorphic `UserReference` lookup semantics.
|
||||
- `EnvelopeReceiverReadOnly` (`TBSIG_ENVELOPE_RECEIVER_READ_ONLY`) stores temporary read-only delegation with validity window (`DateValid`).
|
||||
|
||||
#### Important Constraints From `EGDbContext`
|
||||
- `Envelope.Uuid` has a unique index.
|
||||
- `Receiver.Signature` has a unique index.
|
||||
- `Receiver.EmailAddress` has a unique index.
|
||||
- `EnvelopeReceiver` uses composite primary key: `{ EnvelopeId, ReceiverId }`.
|
||||
- `History.UserReference` is mapped conditionally (`IsMigration == false`) to:
|
||||
- `Receiver.EmailAddress` (`History.Receiver`)
|
||||
- `User.Email` from UserManager (`History.Sender`)
|
||||
- `EnvelopeReceiverReadOnly.AddedWho` maps to `Receiver.EmailAddress` (principal key mapping, not receiver GUID).
|
||||
|
||||
#### Coordinate-Critical Fields
|
||||
- Signature placement is persisted in `DocReceiverElement` as `X`, `Y`, `Width`, `Height`, `Page`.
|
||||
- These coordinates are in INCHES (not PDF points, not pixels).
|
||||
|
||||
#### Cross-Module Tables in Same Context
|
||||
- `EGDbContext` also includes UserManager entities: `User`, `Group`, `Module`, `GroupOfUser`, `ModuleOfUser`, `UserRep`, and `ClientUser`.
|
||||
- Mail dispatcher entity `EmailOut` is also registered under `#if NET` and trigger-configurable.
|
||||
|
||||
#### Trigger-Aware Model Notes
|
||||
- Trigger names are injected from `DbTriggerParams` in appsettings and applied via `ToTable(tb => tb.HasTrigger(...))`.
|
||||
- Current trigger registration is explicit (`AddTrigger<T>()` calls), not attribute/reflection driven.
|
||||
- Do not remove trigger mappings casually; this project depends on DB-side automation/audit behavior.
|
||||
|
||||
## DevExpress
|
||||
|
||||
**License:** Commercial (v25.2.3)
|
||||
@@ -320,5 +411,4 @@ Each envelope maintains independent authentication state.
|
||||
None required. All config in `appsettings.json`.
|
||||
|
||||
**Local dev ports:**
|
||||
- API: `https://localhost:8088`
|
||||
- WebUI: `https://localhost:5131` or `http://localhost:7192`
|
||||
- App host (UI + API): `https://localhost:5131` or `http://localhost:7192`
|
||||
|
||||
@@ -39,7 +39,7 @@ This project contains:
|
||||
- `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`
|
||||
- `EnvelopeGenerator.API` — deprecated project (no longer part of the active solution/runtime path)
|
||||
|
||||
### Legacy / Do Not Touch
|
||||
- `EnvelopeGenerator.Service`
|
||||
@@ -121,7 +121,7 @@ Current controller set includes:
|
||||
- `SignatureController`
|
||||
- `TfaRegistrationController`
|
||||
|
||||
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||
|
||||
---
|
||||
|
||||
|
||||
384
DATABASE_STRUCTURE_CONTEXT.md
Normal file
384
DATABASE_STRUCTURE_CONTEXT.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# Database Structure Context
|
||||
|
||||
## Why this file exists
|
||||
|
||||
This is the deep-dive DB reference for migration work when schema can change and old DB knowledge may be lost.
|
||||
|
||||
- Scope: `EnvelopeGenerator.Domain` entities + `EnvelopeGenerator.Infrastructure/EGDbContext.cs` + live DB metadata snapshot from `DD_ECM`.
|
||||
- Goal: make schema transitions predictable (what exists, how it is linked, where logic executes).
|
||||
- Safety: all DB checks used read-only `SELECT` metadata queries; no data mutation.
|
||||
|
||||
## Snapshot metadata
|
||||
|
||||
- Source DB: `DD_ECM` on `SDD-VHP04-SQL19\DD_TESTING01`
|
||||
- Snapshot date: `2026-10-20`
|
||||
- App context: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
|
||||
|
||||
## Fast navigation
|
||||
|
||||
- High-level aggregate map: `AGENTS.md` -> `Database` section
|
||||
- EF model config source: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
|
||||
- Stored procedure wrappers:
|
||||
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
|
||||
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
|
||||
- `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
|
||||
- Direct SQL execution in controller:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
|
||||
|
||||
## Core data model (ER-level)
|
||||
|
||||
- `Envelope` (`TBSIG_ENVELOPE`) is root aggregate.
|
||||
- `Envelope (1) -> (N) Document` via `TBSIG_ENVELOPE_DOCUMENT.ENVELOPE_ID`.
|
||||
- `Envelope (1) -> (N) History` via `TBSIG_ENVELOPE_HISTORY.ENVELOPE_ID`.
|
||||
- `Envelope (N) <-> (N) Receiver` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{ENVELOPE_ID, RECEIVER_ID}`).
|
||||
- `Document (1) -> (N) DocReceiverElement` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT.DOCUMENT_ID`.
|
||||
- `DocReceiverElement (1) -> (N) ElementAnnotation` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION.ELEMENT_ID`.
|
||||
- `DocumentStatus` links `Envelope + Receiver` state in `TBSIG_DOCUMENT_STATUS`.
|
||||
|
||||
## Entity <-> table mapping (code-level)
|
||||
|
||||
| Entity | Table | PK in code | PK in DB | Important relation notes |
|
||||
|---|---|---|---|---|
|
||||
| `Envelope` | `TBSIG_ENVELOPE` | `Id` | `GUID` | Has `Documents`, `Histories`, `EnvelopeReceivers`; unique index on `Uuid` configured in EF |
|
||||
| `EnvelopeReceiver` | `TBSIG_ENVELOPE_RECEIVER` | `{EnvelopeId, ReceiverId}` | `{ENVELOPE_ID, RECEIVER_ID}` | DB also has `GUID` identity (not modeled as entity PK) |
|
||||
| `Document` | `TBSIG_ENVELOPE_DOCUMENT` | `Id` | `GUID` | FK `ENVELOPE_ID` |
|
||||
| `DocReceiverElement` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT` | `Id` | `GUID` | Coordinates persisted here (`POSITION_X/Y`, `WIDTH`, `HEIGHT`, `PAGE`) |
|
||||
| `ElementAnnotation` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION` | `Id` | `GUID` | FK `ELEMENT_ID` |
|
||||
| `DocumentStatus` | `TBSIG_DOCUMENT_STATUS` | `Id` | `GUID` | FK `ENVELOPE_ID`, `RECEIVER_ID` |
|
||||
| `History` | `TBSIG_ENVELOPE_HISTORY` | `Id` | `GUID` | `USER_REFERENCE` maps to `Receiver.EmailAddress` and `User.Email` when `IsMigration == false` |
|
||||
| `Receiver` | `TBSIG_RECEIVER` | `Id` | `GUID` | Unique indexes in code: `Signature`, `EmailAddress` |
|
||||
| `EnvelopeReceiverReadOnly` | `TBSIG_ENVELOPE_RECEIVER_READ_ONLY` | `Id` | `GUID` | `AddedWho` is FK-like mapping to `Receiver.EmailAddress` |
|
||||
| `EnvelopeType` | `TBSIG_ENVELOPE_TYPE` | `Id` | `GUID` | Used as optional envelope defaults/template |
|
||||
| `EmailTemplate` | `TBSIG_EMAIL_TEMPLATE` | `Id` | `GUID` | Trigger-managed `CHANGED_WHEN` |
|
||||
| `EmailOut` (legacy/bridge) | `TBEMLP_EMAIL_OUT` | `Id` | `GUID` | Internal/obsolete domain class, still registered for DB access |
|
||||
|
||||
## Physical schema snapshot (table/column level)
|
||||
|
||||
Notes:
|
||||
- Format per column: `COLUMN_NAME TYPE NULLABLE`.
|
||||
- This section reflects live DB metadata at snapshot time.
|
||||
|
||||
### `TBSIG_ENVELOPE`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `USER_ID int NOT NULL`
|
||||
- `STATUS int NOT NULL`
|
||||
- `ENVELOPE_UUID nvarchar(36) NOT NULL`
|
||||
- `MESSAGE nvarchar(max) NOT NULL`
|
||||
- `EXPIRES_WHEN datetime NULL`
|
||||
- `EXPIRES_WARNING_WHEN datetime NULL`
|
||||
- `VERIFICATION_TYPE tinyint NOT NULL`
|
||||
- `TITLE nvarchar(128) NULL`
|
||||
- `CONTRACT_TYPE int NULL`
|
||||
- `LANGUAGE nvarchar(5) NULL`
|
||||
- `SEND_REMINDER_EMAILS bit NULL`
|
||||
- `FIRST_REMINDER_DAYS int NULL`
|
||||
- `REMINDER_INTERVAL_DAYS int NULL`
|
||||
- `ENVELOPE_TYPE int NULL`
|
||||
- `CERTIFICATION_TYPE int NULL`
|
||||
- `USE_ACCESS_CODE bit NULL`
|
||||
- `FINAL_EMAIL_TO_CREATOR int NULL`
|
||||
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
|
||||
- `EXPIRES_WHEN_DAYS int NULL`
|
||||
- `EXPIRES_WARNING_WHEN_DAYS int NULL`
|
||||
- `TFA_ENABLED bit NOT NULL`
|
||||
- `DOC_RESULT varbinary(max) NULL`
|
||||
- `LANG_CODE varchar(5) NOT NULL`
|
||||
- `SOURCE varchar(10) NOT NULL`
|
||||
- `COMMENT varchar(128) NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
|
||||
### `TBSIG_ENVELOPE_RECEIVER`
|
||||
|
||||
- `ENVELOPE_ID int NOT NULL`
|
||||
- `RECEIVER_ID int NOT NULL`
|
||||
- `SEQUENCE int NOT NULL`
|
||||
- `NAME nvarchar(128) NULL`
|
||||
- `JOB_TITLE nvarchar(128) NULL`
|
||||
- `COMPANY_NAME nvarchar(128) NULL`
|
||||
- `PRIVATE_MESSAGE nvarchar(max) NULL`
|
||||
- `ACCESS_CODE nvarchar(64) NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
- `GUID bigint NOT NULL`
|
||||
- `PHONE_NUMBER varchar(20) NULL`
|
||||
- `PARENT_ENVELOPE_RECEIVER_ID int NOT NULL`
|
||||
|
||||
### `TBSIG_ENVELOPE_DOCUMENT`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `ENVELOPE_ID int NOT NULL`
|
||||
- `FILENAME nvarchar(256) NOT NULL`
|
||||
- `FILEPATH nvarchar(256) NOT NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `FILENAME_ORIGINAL nvarchar(256) NULL`
|
||||
- `BYTE_DATA varbinary(max) NULL`
|
||||
|
||||
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `DOCUMENT_ID int NOT NULL`
|
||||
- `RECEIVER_ID int NOT NULL`
|
||||
- `ELEMENT_TYPE int NOT NULL`
|
||||
- `POSITION_X float NOT NULL`
|
||||
- `POSITION_Y float NOT NULL`
|
||||
- `WIDTH float NOT NULL`
|
||||
- `HEIGHT float NOT NULL`
|
||||
- `PAGE int NOT NULL`
|
||||
- `REQUIRED bit NOT NULL`
|
||||
- `TOOLTIP nvarchar(512) NULL`
|
||||
- `READ_ONLY bit NOT NULL`
|
||||
- `ANNOTATION_INDEX int NOT NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
- `CHANGED_WHO nvarchar(100) NULL`
|
||||
- `FULL_NAME nvarchar(100) NULL`
|
||||
- `POSITION nvarchar(100) NULL`
|
||||
- `PLACE nvarchar(100) NULL`
|
||||
- `INK varbinary(max) NULL`
|
||||
|
||||
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`
|
||||
|
||||
- `GUID bigint NOT NULL`
|
||||
- `ELEMENT_ID int NOT NULL`
|
||||
- `NAME nvarchar(100) NOT NULL`
|
||||
- `VALUE nvarchar(max) NOT NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
- `CHANGED_WHO nvarchar(100) NULL`
|
||||
- `TYPE nvarchar(50) NOT NULL`
|
||||
- `POSITION_X float NULL`
|
||||
- `POSITION_Y float NULL`
|
||||
- `WIDTH float NULL`
|
||||
- `HEIGHT float NULL`
|
||||
|
||||
### `TBSIG_DOCUMENT_STATUS`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `ENVELOPE_ID int NOT NULL`
|
||||
- `RECEIVER_ID int NOT NULL`
|
||||
- `STATUS int NOT NULL`
|
||||
- `STATUS_CHANGED_WHEN datetime NULL`
|
||||
- `VALUE nvarchar(max) NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
|
||||
### `TBSIG_ENVELOPE_HISTORY`
|
||||
|
||||
- `GUID bigint NOT NULL`
|
||||
- `ENVELOPE_ID int NOT NULL`
|
||||
- `USER_REFERENCE nvarchar(128) NOT NULL`
|
||||
- `STATUS int NOT NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `ACTION_DATE datetime NULL`
|
||||
- `COMMENT nvarchar(max) NULL`
|
||||
|
||||
### `TBSIG_RECEIVER`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `EMAIL_ADDRESS nvarchar(128) NOT NULL`
|
||||
- `SIGNATURE nvarchar(64) NOT NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `TOTP_SECRET_KEY nvarchar(max) NULL`
|
||||
- `TFA_REG_DEADLINE datetime NULL`
|
||||
- `CULTURE nvarchar(10) NULL`
|
||||
|
||||
### `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`
|
||||
|
||||
- `GUID bigint NOT NULL`
|
||||
- `ENVELOPE_ID bigint NOT NULL`
|
||||
- `RECEIVER_MAIL nvarchar(250) NOT NULL`
|
||||
- `DATE_VALID date NOT NULL`
|
||||
- `ADDED_WHO varchar(100) NOT NULL`
|
||||
- `ADDED_WHEN datetime NULL`
|
||||
- `CHANGED_WHO varchar(100) NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
|
||||
### `TBSIG_ENVELOPE_TYPE`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `TITLE nvarchar(128) NOT NULL`
|
||||
- `LANGUAGE nvarchar(5) NOT NULL`
|
||||
- `EXPIRES_DAYS int NULL`
|
||||
- `CERTIFICATION_TYPE int NULL`
|
||||
- `USE_ACCESS_CODE bit NULL`
|
||||
- `FINAL_EMAIL_TO_CREATOR int NULL`
|
||||
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
- `EXPIRES_WARNING_DAYS int NULL`
|
||||
- `SEND_REMINDER_EMAILS bit NULL`
|
||||
- `FIRST_REMINDER_DAYS int NULL`
|
||||
- `REMINDER_INTERVAL_DAYS int NULL`
|
||||
- `CONTRACT_TYPE int NULL`
|
||||
|
||||
### `TBSIG_EMAIL_TEMPLATE`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `NAME nvarchar(64) NULL`
|
||||
- `BODY nvarchar(max) NULL`
|
||||
- `SUBJECT nvarchar(512) NULL`
|
||||
- `ADDED_WHEN datetime NOT NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
- `LANG_CODE varchar(5) NOT NULL`
|
||||
|
||||
### `TBEMLP_EMAIL_OUT`
|
||||
|
||||
- `GUID int NOT NULL`
|
||||
- `REMINDER_TYPE_ID int NOT NULL`
|
||||
- `SENDING_PROFILE int NOT NULL`
|
||||
- `REFERENCE_ID int NOT NULL`
|
||||
- `REFERENCE_STRING varchar(200) NULL`
|
||||
- `ENTITY_ID int NULL`
|
||||
- `WF_ID int NOT NULL`
|
||||
- `WF_REFERENCE varchar(200) NULL`
|
||||
- `EMAIL_ADRESS varchar(1000) NOT NULL`
|
||||
- `EMAIL_SUBJ varchar(500) NOT NULL`
|
||||
- `EMAIL_BODY varchar(max) NOT NULL`
|
||||
- `EMAIL_ATTMT1 varchar(512) NULL`
|
||||
- `EMAIL_SENT datetime NULL`
|
||||
- `COMMENT varchar(500) NULL`
|
||||
- `ADDED_WHO varchar(50) NOT NULL`
|
||||
- `ADDED_WHEN datetime NULL`
|
||||
- `CHANGED_WHO varchar(50) NULL`
|
||||
- `CHANGED_WHEN datetime NULL`
|
||||
- `ERROR_TIMESTAMP datetime NULL`
|
||||
- `ERROR_MSG varchar(900) NULL`
|
||||
- `ATTACHMENT1_BYTES varbinary(max) NULL`
|
||||
- `ATT1_RELATED_ID bigint NULL`
|
||||
- `ATT1_REL_TYPE varchar(20) NULL`
|
||||
|
||||
## Constraints and indexes (relevant)
|
||||
|
||||
From EF + DB metadata snapshot:
|
||||
|
||||
- `TBSIG_ENVELOPE`: PK `PK_TBSIG_ENVELOPE (GUID)`.
|
||||
- `TBSIG_ENVELOPE_RECEIVER`: composite PK `PK_TBSIG_ENVELOPE_RECEIVER (ENVELOPE_ID, RECEIVER_ID)`.
|
||||
- `TBSIG_DOCUMENT_RECEIVER_ELEMENT`: PK `GUID`, plus unique key `UQTBSIG_DOCUMENT_RECEIVER_ELEMENT_DocID_PosX_PosY (DOCUMENT_ID, POSITION_X, POSITION_Y)`.
|
||||
- `TBSIG_RECEIVER`: PK `GUID`, unique key `UQTBSIG_RECEIVER (EMAIL_ADDRESS)`.
|
||||
- `TBEMLP_EMAIL_OUT`: PK `GUID`, FK `FK_TBEMLP_EMAIL_OUT_SENDING_PROFILE`.
|
||||
|
||||
## Stored procedures: behavior and code call sites
|
||||
|
||||
### Active in .NET code path
|
||||
|
||||
1) `PRSIG_API_CREATE_ENVELOPE`
|
||||
- Purpose: creates envelope row (`STATUS=1002`, `SOURCE='API'`, defaults) and returns `ENVELOPE_UUID` via output.
|
||||
- DB writes: `TBSIG_ENVELOPE`.
|
||||
- Called by:
|
||||
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
|
||||
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeExecutor.cs`
|
||||
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
|
||||
|
||||
2) `PRSIG_API_CREATE_RECEIVER`
|
||||
- Purpose: upserts `TBSIG_RECEIVER` by email, adds envelope assignment row with generated access code.
|
||||
- DB writes: `TBSIG_RECEIVER`, `TBSIG_ENVELOPE_RECEIVER`.
|
||||
- Called by:
|
||||
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
|
||||
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeReceiverExecutor.cs`
|
||||
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
|
||||
|
||||
3) `PRSIG_API_ADD_DOC`
|
||||
- Purpose: stores one document for envelope if absent; returns doc id.
|
||||
- DB writes: `TBSIG_ENVELOPE_DOCUMENT`.
|
||||
- Called by:
|
||||
- SQL template `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
|
||||
- executor `EnvelopeGenerator.Infrastructure/Executor/DocumentExecutor.cs`
|
||||
- used in `EnvelopeReceiverController.CreateAsync`.
|
||||
|
||||
4) `PRSIG_API_ADD_DOC_RECEIVER_ELEM`
|
||||
- Purpose: inserts signature element record if `(DOC_ID, RECEIVER_ID)` pair does not exist.
|
||||
- DB writes: `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
|
||||
- Called directly by:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
|
||||
|
||||
5) `PRSIG_API_ADD_HISTORY_STATE`
|
||||
- Purpose: appends history row with status and user reference.
|
||||
- DB writes: `TBSIG_ENVELOPE_HISTORY`.
|
||||
- Called directly by:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
|
||||
|
||||
### Legacy/other path references in repo
|
||||
|
||||
- `PRSIG_GET_ENVELOPES_FOR_USER` and `PRSIG_GET_CLOSED_ENVELOPES_FOR_USER` are called in legacy VB path:
|
||||
- `EnvelopeGenerator.CommonServices/Models/EnvelopeModel.vb`
|
||||
|
||||
- `PRSIG_API_WITHDRAW_ENVELOPE` exists in DB snapshot but no active `.cs` call site found in current server flow.
|
||||
|
||||
## Trigger behavior and where they are wired
|
||||
|
||||
## How trigger names are wired in app
|
||||
|
||||
- Config source: `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json` -> `DbTriggerParams`.
|
||||
- DI binding: `EnvelopeGenerator.Infrastructure/DependencyInjection.cs` (`AddDbTriggerParams`).
|
||||
- Model binding: `EnvelopeGenerator.Infrastructure/EGDbContext.cs` -> `AddTrigger<T>()` -> `ToTable(tb => tb.HasTrigger(triggerName))`.
|
||||
|
||||
### Trigger definitions observed in DB snapshot
|
||||
|
||||
1) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS`
|
||||
- Fires on insert of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
|
||||
- Normalizes/rewrites `POSITION_X`, `POSITION_Y` and sets `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_INS'`.
|
||||
|
||||
2) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD`
|
||||
- Fires on update of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
|
||||
- Rewrites coordinates and `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_UPD'` except when `CHANGED_WHO='OFFSET_Y_POS'`.
|
||||
|
||||
3) `TBSIG_ENVELOPE_HISTORY_AFT_INS`
|
||||
- Fires on history insert.
|
||||
- Central state machine side-effects:
|
||||
- syncs `TBSIG_ENVELOPE.STATUS` for selected `100x` states,
|
||||
- on `1003` applies Y-offset update to receiver elements,
|
||||
- on `2005` calculates partial/fully signed and may insert follow-up `1006` history,
|
||||
- on `2007` (type-dependent reject path) writes rejection emails into `TBEMLP_EMAIL_OUT` and updates envelope rejected state,
|
||||
- on `3001` invitation-message counting can move envelope to `1004`.
|
||||
|
||||
4) `TBSIG_EMAIL_TEMPLATE_AFT_UPD`
|
||||
- Fires on update of `TBSIG_EMAIL_TEMPLATE`.
|
||||
- Sets `CHANGED_WHEN = GETDATE()`.
|
||||
|
||||
5) `TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD`
|
||||
- Fires on update of `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`.
|
||||
- Updates config timestamp row (`TBSIG_CONFIG.GUID = 1`).
|
||||
|
||||
6) `TBEMLP_EMAIL_OUT_AFT_UPD`
|
||||
- Fires on update of `TBEMLP_EMAIL_OUT`.
|
||||
- When `EMAIL_SENT` changes and `WF_ID in (3001..3005)`, writes corresponding history entries to `TBSIG_ENVELOPE_HISTORY`.
|
||||
|
||||
### Trigger mismatch alert (important)
|
||||
|
||||
- `DbTriggerParams` contains `TBEMLP_EMAIL_OUT_AFT_INS`, but this trigger was not found in the snapshot DB (`ExistsInDb = 0`).
|
||||
- `TBEMLP_EMAIL_OUT_AFT_UPD` exists and is active.
|
||||
|
||||
This should be treated as environment drift and validated before migrations.
|
||||
|
||||
## Known model-vs-DB drift risks
|
||||
|
||||
These are not necessarily bugs, but they are migration risk hotspots:
|
||||
|
||||
- `Receiver.EmailAddress` is `[StringLength(250)]` in entity, DB column is `nvarchar(128)`.
|
||||
- `EnvelopeReceiverReadOnly.AddedWho` entity expects `nvarchar(250)`, DB column is `varchar(100)`.
|
||||
- `EnvelopeReceiver` entity does not model DB columns `GUID` and `PARENT_ENVELOPE_RECEIVER_ID`.
|
||||
- `Envelope` entity does not explicitly model some DB columns (`VERIFICATION_TYPE`, `LANG_CODE`, `SOURCE`).
|
||||
- `EmailOut` entity has `[NotMapped] AddedWhen`, while DB has `ADDED_WHEN` column.
|
||||
|
||||
Before schema changes, reconcile these deltas intentionally (either mapping update or documented accepted divergence).
|
||||
|
||||
## Migration-safe operating rules
|
||||
|
||||
- Do not rely on implicit trigger behavior; document expected side effects in feature code touching history/status flows.
|
||||
- Any change to `TBSIG_ENVELOPE_HISTORY` semantics requires trigger impact review first.
|
||||
- Keep `DbTriggerParams` synchronized with actual DB trigger inventory per environment.
|
||||
- For stored procedure changes, update both:
|
||||
- SQL wrapper templates (`Application/Common/SQL/*`)
|
||||
- direct SQL callers in controllers/executors.
|
||||
|
||||
## Refresh checklist (read-only)
|
||||
|
||||
Run these metadata checks before/after DB migration:
|
||||
|
||||
1. Column inventory per table (`INFORMATION_SCHEMA.COLUMNS`).
|
||||
2. Constraint inventory (`INFORMATION_SCHEMA.TABLE_CONSTRAINTS` + `KEY_COLUMN_USAGE`).
|
||||
3. Trigger inventory (`sys.triggers`) and definitions (`OBJECT_DEFINITION`).
|
||||
4. Stored procedure inventory (`sys.procedures`) and definitions (`OBJECT_DEFINITION`).
|
||||
5. Repo call-site grep for `PRSIG_` names to detect dead or missing integration points.
|
||||
@@ -0,0 +1,40 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Behaviors;
|
||||
|
||||
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||
|
||||
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
||||
{
|
||||
_validators = validators;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_validators.Any())
|
||||
{
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
var failures = _validators
|
||||
.Select(v => v.Validate(context))
|
||||
.SelectMany(r => r.Errors)
|
||||
.Where(f => f is not null)
|
||||
.ToList();
|
||||
|
||||
if (failures.Count > 0)
|
||||
{
|
||||
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Configurations;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ESignParams
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string ApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string DefaultReason { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Configurations;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class SignatureFieldParams
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double Width { get; init; } = 1.77;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double Height { get; init; } = 1.96;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
// TODO: Add the “Reason” property and integrate with UI and the digital signature process.
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||
/// </summary>
|
||||
public class DocReceiverElementDto : IDocReceiverElement
|
||||
public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the element.
|
||||
@@ -18,11 +20,21 @@ public class DocReceiverElementDto : IDocReceiverElement
|
||||
/// </summary>
|
||||
public int DocumentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public virtual DocumentDto? Document { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the receiver.
|
||||
/// </summary>
|
||||
public int ReceiverId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public virtual ReceiverDto? Receiver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the element.
|
||||
/// </summary>
|
||||
@@ -122,4 +134,41 @@ public class DocReceiverElementDto : IDocReceiverElement
|
||||
///
|
||||
/// </summary>
|
||||
public byte[]? Ink { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stored signature bytes as an image payload with detected MIME type.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when signature image bytes do not exist.</exception>
|
||||
public (byte[] Bytes, string ContentType, string FileExtension) ToImage()
|
||||
{
|
||||
if (Ink is not { Length: > 0 })
|
||||
throw new InvalidOperationException("Signature image (Ink) does not exist.");
|
||||
|
||||
if (HasPrefix(Ink, 0x89, 0x50, 0x4E, 0x47))
|
||||
return (Ink, "image/png", ".png");
|
||||
|
||||
if (HasPrefix(Ink, 0xFF, 0xD8, 0xFF))
|
||||
return (Ink, "image/jpeg", ".jpg");
|
||||
|
||||
if (HasPrefix(Ink, 0x52, 0x49, 0x46, 0x46) &&
|
||||
Ink.Length >= 12 &&
|
||||
Ink[8] == 0x57 && Ink[9] == 0x45 && Ink[10] == 0x42 && Ink[11] == 0x50)
|
||||
return (Ink, "image/webp", ".webp");
|
||||
|
||||
return (Ink, "application/octet-stream", ".bin");
|
||||
}
|
||||
|
||||
private static bool HasPrefix(byte[] data, params byte[] prefix)
|
||||
{
|
||||
if (data.Length < prefix.Length)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < prefix.Length; i++)
|
||||
{
|
||||
if (data[i] != prefix[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing document metadata and elements without binary payload.
|
||||
/// </summary>
|
||||
public record DocumentDetailsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the document.
|
||||
/// </summary>
|
||||
public int Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the envelope ID to which the document belongs.
|
||||
/// </summary>
|
||||
public int EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the document was added.
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||
/// </summary>
|
||||
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
|
||||
}
|
||||
@@ -3,30 +3,25 @@
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||
/// </summary>
|
||||
public class DocumentDto
|
||||
public record DocumentDto : DocumentDetailsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the document.
|
||||
///
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
public DocumentDto() { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the envelope ID to which the document belongs.
|
||||
///
|
||||
/// </summary>
|
||||
public int EnvelopeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the document was added.
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; set; }
|
||||
/// <param name="byteData"></param>
|
||||
/// <param name="details"></param>
|
||||
public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
|
||||
{
|
||||
ByteData = byteData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the binary data of the document, if available.
|
||||
/// </summary>
|
||||
public byte[]? ByteData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||
/// </summary>
|
||||
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public record EnvelopeDto : IEnvelope
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? ContractType { get; set; }
|
||||
public ContractType? ContractType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default value is 'de-DE'
|
||||
@@ -84,7 +84,42 @@ public record EnvelopeDto : IEnvelope
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? CertificationType { get; set; }
|
||||
public CertificationType? CertificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? FinalEmailToCreator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? FinalEmailToReceivers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool? SendReminderEmails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? FirstReminderDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? ReminderIntervalDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? ExpiresWhenDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? ExpiresWarningWhenDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -135,4 +170,4 @@ public record EnvelopeDto : IEnvelope
|
||||
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||
/// </summary>
|
||||
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ public record EnvelopeReceiverDto
|
||||
/// </summary>
|
||||
public string? PrivateMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? AccessCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -83,4 +88,4 @@ public record EnvelopeReceiverDto
|
||||
h.Receiver?.Id == ReceiverId &&
|
||||
h.Status == EnvelopeStatus.DocumentSigned
|
||||
) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||
@@ -28,7 +30,7 @@ public class EnvelopeTypeDto
|
||||
/// <summary>
|
||||
/// Gets or sets the certification type identifier.
|
||||
/// </summary>
|
||||
public int? CertificationType { get; set; }
|
||||
public CertificationType? CertificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether an access code is required.
|
||||
@@ -78,5 +80,5 @@ public class EnvelopeTypeDto
|
||||
/// <summary>
|
||||
/// Gets or sets the contract type associated with the envelope type.
|
||||
/// </summary>
|
||||
public int? ContractType { get; set; }
|
||||
public ContractType? ContractType { get; set; }
|
||||
}
|
||||
16
EnvelopeGenerator.Application/Common/Dto/PdfPageImageDto.cs
Normal file
16
EnvelopeGenerator.Application/Common/Dto/PdfPageImageDto.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
public sealed class PdfPageImageDto
|
||||
{
|
||||
public int PageNumber { get; init; }
|
||||
|
||||
public double WidthPt { get; init; }
|
||||
|
||||
public double HeightPt { get; init; }
|
||||
|
||||
public int WidthPx { get; init; }
|
||||
|
||||
public int HeightPx { get; init; }
|
||||
|
||||
public string ImageDataUrl { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
public sealed class PdfRenderRequestDto
|
||||
{
|
||||
public byte[]? PdfBytes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using EnvelopeGenerator.Application.Common.Enums;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public sealed record PdfSignOptionsDto
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public long DocumentElementId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Reason { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Location { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string ApplicationName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public PdfSignatureCertificationLevel CertificationLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public PdfSignatureAppearanceDto? Appearance { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public enum PdfSignatureCoordinateUnit
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
Point = 0,
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
Inch = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public enum PdfSignatureCoordinateOrigin
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
BottomLeft = 0,
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
TopLeft = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="PageNumber"></param>
|
||||
/// <param name="Left"></param>
|
||||
/// <param name="Bottom"></param>
|
||||
/// <param name="Width"></param>
|
||||
/// <param name="Height"></param>
|
||||
/// <param name="ShowLabels"></param>
|
||||
/// <param name="ShowName"></param>
|
||||
/// <param name="ShowDate"></param>
|
||||
/// <param name="ShowReason"></param>
|
||||
/// <param name="ShowLocation"></param>
|
||||
/// <param name="ShowDistinguishedName"></param>
|
||||
/// <param name="DateTimeFormat"></param>
|
||||
/// <param name="CoordinateUnit"></param>
|
||||
/// <param name="CoordinateOrigin"></param>
|
||||
public sealed record PdfSignatureAppearanceDto(
|
||||
int PageNumber,
|
||||
double Left,
|
||||
double Bottom,
|
||||
double Width,
|
||||
double Height,
|
||||
bool ShowLabels = false,
|
||||
bool ShowName = false,
|
||||
bool ShowDate = false,
|
||||
bool ShowReason = false,
|
||||
bool ShowLocation = false,
|
||||
bool ShowDistinguishedName = false,
|
||||
string DateTimeFormat = "dd.MM.yyyy HH:mm:ss zzz",
|
||||
PdfSignatureCoordinateUnit CoordinateUnit = PdfSignatureCoordinateUnit.Point,
|
||||
PdfSignatureCoordinateOrigin CoordinateOrigin = PdfSignatureCoordinateOrigin.BottomLeft)
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="FamilyName"></param>
|
||||
/// <param name="Size"></param>
|
||||
/// <param name="IsBold"></param>
|
||||
/// <param name="IsItalic"></param>
|
||||
/// <param name="IsUnderline"></param>
|
||||
public sealed record FontDto(string FamilyName = "Helvetica", float Size = 10, bool IsBold = false, bool IsItalic = false, bool IsUnderline = false)
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public RgbaColorDto Color { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="R"></param>
|
||||
/// <param name="G"></param>
|
||||
/// <param name="B"></param>
|
||||
public sealed record RgbaColorDto(double R = 0, double G = 0, double B = 0);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public FontDto Font { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
public sealed record PdfSignaturePrincipalDto(
|
||||
string? CommonName,
|
||||
string? Organization,
|
||||
string? Country,
|
||||
string? Full);
|
||||
|
||||
public sealed record PdfSignatureCertificateInfoDto(
|
||||
PdfSignaturePrincipalDto IssuedTo,
|
||||
PdfSignaturePrincipalDto IssuedBy,
|
||||
string? SerialNumber,
|
||||
DateTimeOffset? ValidFrom,
|
||||
DateTimeOffset? ValidTo,
|
||||
bool SelfSigned,
|
||||
bool Expired,
|
||||
bool NotYetValid,
|
||||
bool IsSigner,
|
||||
int? Version,
|
||||
string? KeyAlgorithm,
|
||||
string? SignatureAlgorithm,
|
||||
string? Pem,
|
||||
int? KeySizeBits);
|
||||
|
||||
public sealed record PdfSignatureCheckItemDto(
|
||||
bool CoversWholeDoc,
|
||||
string? Reason,
|
||||
string? BusinessReason,
|
||||
string? Location,
|
||||
string? DeclaredName,
|
||||
string? ContactInfo,
|
||||
string? Email,
|
||||
string? Phone,
|
||||
string? DigestAlgorithm,
|
||||
string? SignatureAlgorithm,
|
||||
string? SignerName,
|
||||
string? Issuer,
|
||||
string? SerialNumber,
|
||||
DateTimeOffset? ValidFrom,
|
||||
DateTimeOffset? ValidTo,
|
||||
string ExpiryCheckedAgainst,
|
||||
IReadOnlyList<PdfSignatureCertificateInfoDto> Certificates,
|
||||
bool Unchanged,
|
||||
string? FieldName,
|
||||
bool IsCryptographicallyValid,
|
||||
string? SigningTime,
|
||||
string? Filter,
|
||||
string? SubFilter,
|
||||
string? ApplicationName,
|
||||
long? DocumentElementId,
|
||||
string? OriginalDocumentSha256,
|
||||
string? MessageDigestHex,
|
||||
string? Error);
|
||||
|
||||
public sealed record PdfSignatureCheckResultDto(
|
||||
bool Signed,
|
||||
IReadOnlyList<PdfSignatureCheckItemDto> Signatures);
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
|
||||
public class RecentReceiverSuggestionDto
|
||||
{
|
||||
public required string FullName { get; init; }
|
||||
|
||||
public required string EmailAddress { get; init; }
|
||||
|
||||
public required DateTime LastUsedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Enums;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public enum PdfSignatureCertificationLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// All changes are allowed
|
||||
/// </summary>
|
||||
NoCertification = 0,
|
||||
|
||||
/// <summary>
|
||||
/// No changes are allowed
|
||||
/// </summary>
|
||||
NoChangesAllowed,
|
||||
|
||||
/// <summary>
|
||||
/// Allows users to fill in forms and sign the document.
|
||||
/// </summary>
|
||||
FillForms,
|
||||
|
||||
/// <summary>
|
||||
/// Allows users to fill in forms, create, modify and delete annotations, and sign the document.
|
||||
/// </summary>
|
||||
FillFormsAndAnnotate
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IPdfPageImageRenderer
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
Stream ComposeSignaturePreviewPng(ISignatureInkPayload element);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsSupportedImageFormat(byte[] bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class PdfPageImageRendererExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="renderer"></param>
|
||||
/// <param name="inkBytes"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="BadRequestException"></exception>
|
||||
public static byte[] EnsureSupportedImageBytes(this IPdfPageImageRenderer renderer, byte[] inkBytes)
|
||||
{
|
||||
if (!renderer.IsSupportedImageFormat(inkBytes))
|
||||
throw new BadRequestException("Only PNG and JPEG signature formats are supported.");
|
||||
|
||||
return inkBytes;
|
||||
}
|
||||
|
||||
public static byte[] ComposeSignaturePreviewPngBytes(this IPdfPageImageRenderer renderer, ISignatureInkPayload element)
|
||||
{
|
||||
using var stream = renderer.ComposeSignaturePreviewPng(element);
|
||||
|
||||
if (stream is MemoryStream memoryStream)
|
||||
return memoryStream.ToArray();
|
||||
|
||||
using var copy = new MemoryStream();
|
||||
stream.CopyTo(copy);
|
||||
return copy.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IRequestContext
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
string? Origin { get; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines PDF digital-signing operations for the application boundary.
|
||||
/// </summary>
|
||||
public interface IPdfDigitalSignatureService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a digital signature for the provided PDF bytes and returns
|
||||
/// a new signed PDF document.
|
||||
/// </summary>
|
||||
/// <param name="inputPdf">Source PDF content stream.</param>
|
||||
/// <param name="outputPdf">Destination stream for signed PDF output.</param>
|
||||
/// <param name="options">Optional signing options that map to PDF signature metadata fields.</param>
|
||||
/// <param name="image">Optional image stream for signature appearance.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
||||
Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the provided PDF contains signature field(s) and whether
|
||||
/// those signatures are cryptographically valid.
|
||||
/// </summary>
|
||||
/// <param name="inputPdf">PDF content stream to inspect.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>A structured result containing signature presence and per-signature details.</returns>
|
||||
Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record DocumentReceivedNotification : EnvelopeNotificationBase
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string DocumentTitle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string NameReceiver { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string NameSender { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// [SIGNATURE_TYPE]
|
||||
/// </summary>
|
||||
public required bool IsReadAndConfirm { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string SignatureType => IsReadAndConfirm ? "Signieren" : "Lesen und bestätigen";
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required bool AdvancedSignature { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string DocumentProcess => AdvancedSignature ? " und elektronisch unterschreiben" : string.Empty;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required Uri Origin { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string EnvelopeUuid { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string ReceiverSignature { private get; init; }
|
||||
|
||||
private Uri EnvelopeURL => new(Origin, $"Envelope/{(EnvelopeUuid, ReceiverSignature).ToEnvelopeKey()}");
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string LinkToDocument => EnvelopeURL.AbsoluteUri;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string LinkToDocumentText
|
||||
{
|
||||
get
|
||||
{
|
||||
var urlText = EnvelopeURL.ToString();
|
||||
return $"{urlText[..Math.Min(urlText.Length, 40)]}..";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string Message { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string NamePortal { get; set; } = "signFLOW Portal";
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override EmailTemplateType GetTemplateType()
|
||||
{
|
||||
return EmailTemplateType.DocumentReceived;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record DocumentSignedNotification : EnvelopeNotificationBase
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string DocumentTitle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string NameReceiver { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string NamePortal { get; init; } = "signFLOW Portal";
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required bool IsReadAndConfirm { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string ReceiverAction => IsReadAndConfirm ? "bestätigt" : "unterschrieben";
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string FinalStatus => IsReadAndConfirm ? "Lesebestätigung" : "Signatur";
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string FinalAction => IsReadAndConfirm ? "Empfänger bestätigt" : "Vertragspartner unterzeichnet";
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public override EmailTemplateType GetTemplateType() => EmailTemplateType.DocumentSigned;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Umschlag abgeschlossen: '[DOCUMENT_TITLE]'
|
||||
|
||||
---
|
||||
|
||||
Guten Tag [NAME_RECEIVER],<br /> <br /> Der Vorgang <B><I>'[DOCUMENT_TITLE]'</I></B> wurde erfolgreich abgeschlossen.<br /> <br /> Sie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,19 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public abstract record EnvelopeNotificationBase
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public abstract EmailTemplateType GetTemplateType();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IReadOnlyList<(string FileName, byte[] Content)>? Attachments { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IEnvelopeNotificationService
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvelopeNotificationBase"></typeparam>
|
||||
/// <param name="receiverEmail"></param>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
|
||||
where TEnvelopeNotificationBase : EnvelopeNotificationBase;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public class MappingProfile : Profile
|
||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||
CreateMap<Document, DocumentDto>();
|
||||
CreateMap<Document, DocumentDetailsDto>();
|
||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||
@@ -68,4 +69,4 @@ public class MappingProfile : Profile
|
||||
? new SmsResponse() { Ok = true }
|
||||
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
@@ -96,6 +97,10 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
|
||||
{
|
||||
// do not send email if the envelope is deleted and the status is less than or equal to EnvelopeSaved
|
||||
if (notification is EnvelopeDeletedNotification delNot && delNot.Envelope.Status <= EnvelopeStatus.EnvelopeSaved)
|
||||
return;
|
||||
|
||||
var placeHolders = CreatePlaceHolders(notification);
|
||||
|
||||
var temp = await TempRepo
|
||||
|
||||
@@ -10,6 +10,8 @@ using System.Reflection;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||
using EnvelopeGenerator.Application.Common.Behaviors;
|
||||
using FluentValidation;
|
||||
|
||||
namespace EnvelopeGenerator.Application;
|
||||
|
||||
@@ -47,8 +49,10 @@ public static class DependencyInjection
|
||||
|
||||
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
|
||||
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
||||
services.Configure<SignatureFieldParams>(config.GetSection(nameof(SignatureFieldParams)));
|
||||
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
|
||||
|
||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||
@@ -59,6 +63,9 @@ public static class DependencyInjection
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
// Generic request validation
|
||||
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
||||
|
||||
// Register SignCommand pipeline behaviors in execution order
|
||||
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
||||
@@ -80,6 +87,8 @@ public static class DependencyInjection
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||
});
|
||||
|
||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,15 @@ namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
|
||||
/// </summary>
|
||||
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? Id { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool IncludeDocument { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,7 +56,10 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
|
||||
{
|
||||
var q = _repository.Query;
|
||||
|
||||
if(request.Envelope.Id is int envelopeId)
|
||||
if (request.Id is not null)
|
||||
q = q.Where(e => e.Id == request.Id);
|
||||
|
||||
if (request.Envelope.Id is int envelopeId)
|
||||
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
|
||||
|
||||
if (request.Envelope.Uuid is string envelopeUuid)
|
||||
@@ -59,6 +71,9 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
|
||||
if (request.Receiver.Signature is string signature)
|
||||
q = q.Where(e => e.Receiver.Signature == signature);
|
||||
|
||||
if( request.IncludeDocument)
|
||||
q = q.Include(e => e.Document);
|
||||
|
||||
var elements = await q.ToListAsync(cancellationToken);
|
||||
|
||||
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a query to read document metadata/details without binary payload.
|
||||
/// </summary>
|
||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
|
||||
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
|
||||
|
||||
/// <summary>
|
||||
/// Handles queries for reading document details without loading BYTE_DATA.
|
||||
/// </summary>
|
||||
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
||||
{
|
||||
private readonly IRepository<Document> _repo;
|
||||
|
||||
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
||||
{
|
||||
var docQuery = _repo.Query.AsNoTracking();
|
||||
|
||||
if (query.UserId is int userId)
|
||||
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||
|
||||
// shortcut: manual select avoids AutoMapper ProjectTo pulling BYTE_DATA and the full navigation
|
||||
// graph (Envelope → History → User etc.) which caused an extremely slow query (~87s).
|
||||
// Annotations navigation is IEnumerable<T> on the entity (not ICollection<T>), so EF Core
|
||||
// cannot translate it in a Select projection — we skip it entirely here; sender editor only
|
||||
// needs element coordinates, not annotation payloads.
|
||||
var raw = await docQuery
|
||||
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
||||
.Select(d => new
|
||||
{
|
||||
d.Id,
|
||||
d.EnvelopeId,
|
||||
d.AddedWhen,
|
||||
Elements = d.Elements == null ? null : d.Elements.Select(e => new
|
||||
{
|
||||
e.Id,
|
||||
e.DocumentId,
|
||||
e.ReceiverId,
|
||||
e.ElementType,
|
||||
e.X,
|
||||
e.Y,
|
||||
e.Width,
|
||||
e.Height,
|
||||
e.Page,
|
||||
e.Required,
|
||||
e.Tooltip,
|
||||
e.ReadOnly,
|
||||
e.AnnotationIndex,
|
||||
e.AddedWhen,
|
||||
e.ChangedWhen,
|
||||
e.FullName,
|
||||
e.Position,
|
||||
e.Place,
|
||||
}).ToList()
|
||||
})
|
||||
.FirstOrDefaultAsync(cancel);
|
||||
|
||||
if (raw is null)
|
||||
return null;
|
||||
|
||||
return new DocumentDetailsDto
|
||||
{
|
||||
Id = raw.Id,
|
||||
EnvelopeId = raw.EnvelopeId,
|
||||
AddedWhen = raw.AddedWhen,
|
||||
Elements = raw.Elements?.Select(e => new DocReceiverElementDto
|
||||
{
|
||||
Id = e.Id,
|
||||
DocumentId = e.DocumentId,
|
||||
ReceiverId = e.ReceiverId,
|
||||
ElementType = e.ElementType,
|
||||
X = e.X,
|
||||
Y = e.Y,
|
||||
Width = e.Width,
|
||||
Height = e.Height,
|
||||
Page = e.Page,
|
||||
Required = e.Required,
|
||||
Tooltip = e.Tooltip,
|
||||
ReadOnly = e.ReadOnly,
|
||||
AnnotationIndex = e.AnnotationIndex,
|
||||
AddedWhen = e.AddedWhen,
|
||||
ChangedWhen = e.ChangedWhen,
|
||||
FullName = e.FullName,
|
||||
Position = e.Position,
|
||||
Place = e.Place,
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||
|
||||
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
|
||||
{
|
||||
public ReadDocumentDetailsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.EnvelopeId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("EnvelopeId is required.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||
|
||||
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
|
||||
{
|
||||
public ReadDocumentQueryValidator()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
|
||||
.WithMessage("Id or EnvelopeId is required.");
|
||||
|
||||
RuleFor(x => x.Id)
|
||||
.GreaterThan(0)
|
||||
.When(x => x.Id.HasValue)
|
||||
.WithMessage("Id must be greater than 0.");
|
||||
|
||||
RuleFor(x => x.EnvelopeId)
|
||||
.GreaterThan(0)
|
||||
.When(x => x.EnvelopeId.HasValue)
|
||||
.WithMessage("EnvelopeId must be greater than 0.");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
@@ -19,6 +20,8 @@
|
||||
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
/// Befehl zur Erstellung eines Umschlags.
|
||||
@@ -4,7 +4,7 @@ using MediatR;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the creation of an envelope along with its associated document and recipients.
|
||||
@@ -1,7 +1,7 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -0,0 +1,84 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record RemoveEnvelopeReceiverCommand : IRequest
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required int EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required int ReceiverId { get; init; }
|
||||
|
||||
internal int? UserId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public RemoveEnvelopeReceiverCommand Authenticated(int userId)
|
||||
{
|
||||
UserId = userId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Repo"></param>
|
||||
public class RemoveEnvelopeReceiverCommandHandler(
|
||||
IRepository<EnvelopeReceiver> Repo,
|
||||
IRepository<DocReceiverElement> ElementRepo,
|
||||
IRepository<ElementAnnotation> AnnotRepo) : IRequestHandler<RemoveEnvelopeReceiverCommand>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public async Task Handle(RemoveEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||
{
|
||||
var er = await Repo.Query
|
||||
.Where(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId)
|
||||
.Include(er => er.Envelope)
|
||||
.SingleOrDefaultAsync(cancel)
|
||||
?? throw new NotFoundException();
|
||||
|
||||
if (er.Envelope is null)
|
||||
throw new InvalidOperationException($"In {nameof(RemoveEnvelopeReceiverCommandHandler)} EnvelopeReceiver.Envelope is null." +
|
||||
$"This should not happen." +
|
||||
$"EnvelopeId: {request.EnvelopeId}, ReceiverId: {request.ReceiverId}");
|
||||
|
||||
if (request.UserId is int userId && er.Envelope.UserId != userId)
|
||||
throw new AuthorizationException("Cannot remove receiver from envelope that you do not own.");
|
||||
|
||||
if (er.Envelope.Status > EnvelopeStatus.EnvelopeSaved)
|
||||
throw new ForbiddenException("Cannot remove receiver from envelope that is already processed.");
|
||||
|
||||
// 1. Delete all ElementAnnotations for this EnvelopeReceiver
|
||||
await AnnotRepo.DeleteAsync(ann => ann.Element.Document.EnvelopeId == request.EnvelopeId && ann.Element.ReceiverId == request.ReceiverId, cancel);
|
||||
|
||||
// 2. Delete all DocReceiverElements for this EnvelopeReceiver
|
||||
await ElementRepo.DeleteAsync(el => el.Document.EnvelopeId == request.EnvelopeId && el.ReceiverId == request.ReceiverId, cancel);
|
||||
|
||||
// 3. Delete the EnvelopeReceiver
|
||||
await Repo.DeleteAsync(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId, cancel);
|
||||
}
|
||||
}
|
||||
@@ -16,5 +16,9 @@ public class MappingProfile : Profile
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<Receiver, ReceiverDto>();
|
||||
|
||||
CreateMap<EnvelopeReceiver, RecentReceiverSuggestionDto>()
|
||||
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
|
||||
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Receiver.EmailAddress));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,24 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
/// </remarks>
|
||||
public record ReadEnvelopeReceiverSecretQuery
|
||||
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
|
||||
IRequest<EnvelopeReceiverSecretDto?>;
|
||||
IRequest<EnvelopeReceiverSecretDto?>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public ReadEnvelopeReceiverSecretQuery() { }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeId"></param>
|
||||
/// <param name="receiverId"></param>
|
||||
public ReadEnvelopeReceiverSecretQuery(int envelopeId, int receiverId)
|
||||
{
|
||||
Envelope.Id = envelopeId;
|
||||
Receiver.Id = receiverId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Reads recent receiver suggestions for envelope creation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sender scope is assigned via chain method to avoid exposing username mutation from API query binding.
|
||||
/// </remarks>
|
||||
public record ReadRecentReceiverSuggestionsQuery : IRequest<IReadOnlyList<RecentReceiverSuggestionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Sender username used for scoping suggestion results.
|
||||
/// </summary>
|
||||
internal string? Username { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Desired number of results.
|
||||
/// </summary>
|
||||
public int? Take { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new query instance scoped to the specified sender username.
|
||||
/// </summary>
|
||||
/// <param name="username">Sender username from authenticated claims.</param>
|
||||
/// <returns>Scoped query instance.</returns>
|
||||
public ReadRecentReceiverSuggestionsQuery ForSender(string username)
|
||||
{
|
||||
return this with { Username = username };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="ReadRecentReceiverSuggestionsQuery"/>.
|
||||
/// </summary>
|
||||
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository)
|
||||
: IRequestHandler<ReadRecentReceiverSuggestionsQuery, IReadOnlyList<RecentReceiverSuggestionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves distinct recent receiver suggestions grouped by receiver email.
|
||||
/// </summary>
|
||||
/// <param name="request">Suggestion query.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
/// <returns>Ordered list of recent receiver suggestions.</returns>
|
||||
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancel)
|
||||
{
|
||||
var query = repository.Query
|
||||
.AsNoTracking()
|
||||
.Where(er => er.Receiver != null);
|
||||
|
||||
if (request.Username is not null)
|
||||
query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username);
|
||||
|
||||
// 1) For each email, compute the latest usage timestamp.
|
||||
var latestPerEmail = query
|
||||
.GroupBy(er => er.Receiver!.EmailAddress)
|
||||
.Select(g => new
|
||||
{
|
||||
EmailAddress = g.Key,
|
||||
LastUsedAt = g.Max(er => er.AddedWhen),
|
||||
});
|
||||
|
||||
// 2) Join back to rows that match the latest timestamp per email.
|
||||
var latestRows =
|
||||
from er in query
|
||||
join latest in latestPerEmail
|
||||
on new { EmailAddress = er.Receiver!.EmailAddress, LastUsedAt = er.AddedWhen }
|
||||
equals new { latest.EmailAddress, latest.LastUsedAt }
|
||||
select new
|
||||
{
|
||||
EmailAddress = er.Receiver!.EmailAddress,
|
||||
FullName = er.Name,
|
||||
LastUsedAt = er.AddedWhen,
|
||||
};
|
||||
|
||||
// 3) Tie-break: if multiple rows share the same latest timestamp for an email,
|
||||
// pick a deterministic name via Max(FullName), then order by most recent usage.
|
||||
var suggestions = latestRows
|
||||
.GroupBy(x => new { x.EmailAddress, x.LastUsedAt })
|
||||
.Select(g => new RecentReceiverSuggestionDto
|
||||
{
|
||||
EmailAddress = g.Key.EmailAddress,
|
||||
FullName = g.Max(x => x.FullName) ?? string.Empty,
|
||||
LastUsedAt = g.Key.LastUsedAt,
|
||||
})
|
||||
.OrderByDescending(x => x.LastUsedAt)
|
||||
.ThenBy(x => x.EmailAddress);
|
||||
|
||||
return request.Take is int take
|
||||
? await suggestions.Take(take).ToListAsync(cancel)
|
||||
: await suggestions.ToListAsync(cancel);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using MediatR;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
|
||||
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
||||
{
|
||||
private readonly IRepository<EnvelopeType> _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
|
||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var types = await _repository.Query.ToListAsync(cancellationToken);
|
||||
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
|
||||
// TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
|
||||
// Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
|
||||
return await _repository.Query
|
||||
.AsNoTracking()
|
||||
.Select(t => new EnvelopeTypeDto
|
||||
{
|
||||
Id = t.Id,
|
||||
Title = t.Title ?? string.Empty,
|
||||
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
||||
ExpiresDays = t.ExpiresDays,
|
||||
CertificationType = t.CertificationType,
|
||||
UseAccessCode = t.UseAccessCode,
|
||||
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
||||
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
||||
AddedWhen = t.AddedWhen,
|
||||
ChangedWhen = t.ChangedWhen,
|
||||
ExpiresWarningDays = t.ExpiresWarningDays,
|
||||
SendReminderEmails = t.SendReminderEmails,
|
||||
FirstReminderDays = t.FirstReminderDays,
|
||||
ReminderIntervalDays = t.ReminderIntervalDays,
|
||||
ContractType = t.ContractType
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
@@ -13,23 +13,96 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
/// </summary>
|
||||
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Optionale Umschlag-ID fuer Aktualisierungsvorgaenge.
|
||||
/// </summary>
|
||||
public int? EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Kennung des ausgewaehlten Umschlagtyps (Vorlage).
|
||||
/// </summary>
|
||||
public int? EnvelopeTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sprachcode fuer Umschlaginhalt und Benachrichtigungen.
|
||||
/// </summary>
|
||||
public string? Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gibt an, ob der Zugriffscode fuer Empfaenger aktiviert ist.
|
||||
/// </summary>
|
||||
public bool? UseAccessCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Zertifizierungstyp fuer den Signaturprozess.
|
||||
/// </summary>
|
||||
public CertificationType? CertificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
||||
/// </summary>
|
||||
public int? FinalEmailToCreator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Finale E-Mail-Einstellung fuer die Empfaenger.
|
||||
/// </summary>
|
||||
public int? FinalEmailToReceivers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gibt an, ob Erinnerungs-E-Mails aktiviert sind.
|
||||
/// </summary>
|
||||
public bool? SendReminderEmails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Anzahl der Tage bis zur ersten Erinnerung.
|
||||
/// </summary>
|
||||
public int? FirstReminderDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Intervall in Tagen zwischen Erinnerungs-E-Mails.
|
||||
/// </summary>
|
||||
public int? ReminderIntervalDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Anzahl der Tage bis zum Ablauf des Umschlags.
|
||||
/// </summary>
|
||||
public int? ExpiresWhenDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Anzahl der Tage vor Ablauf, ab denen gewarnt wird.
|
||||
/// </summary>
|
||||
public int? ExpiresWarningWhenDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Title { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Message { get; set; }
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
||||
/// </summary>
|
||||
public bool TFAEnabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gibt an, ob der Umschlag nach dem Speichern sofort versendet werden soll.
|
||||
/// </summary>
|
||||
public bool Send { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optionaler PDF-Dokumentinhalt als Base64.
|
||||
/// </summary>
|
||||
public CreateEnvelopeDocumentDto? Document { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Empfaengerliste inklusive optionaler Signaturfeld-Koordinaten.
|
||||
/// </summary>
|
||||
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// ID des Absenders
|
||||
/// </summary>
|
||||
@@ -55,4 +128,31 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public bool UseSQLExecutor { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="DataAsBase64"></param>
|
||||
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="X"></param>
|
||||
/// <param name="Y"></param>
|
||||
/// <param name="Page"></param>
|
||||
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="EmailAddress"></param>
|
||||
/// <param name="Name"></param>
|
||||
/// <param name="PhoneNumber"></param>
|
||||
/// <param name="Fields"></param>
|
||||
public record CreateEnvelopeReceiverDto(
|
||||
string EmailAddress,
|
||||
string? Name,
|
||||
string? PhoneNumber,
|
||||
List<CreateEnvelopeFieldDto> Fields);
|
||||
|
||||
@@ -1,36 +1,66 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using DigitalData.UserManager.Domain.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Application.Histories.Commands;
|
||||
using EnvelopeGenerator.Domain;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?>
|
||||
/// <param name="Provider"></param>
|
||||
/// <param name="Mapper"></param>
|
||||
/// <param name="EnvNtfService"></param>
|
||||
/// <param name="ReqCntxt"></param>
|
||||
/// <param name="Logger"></param>
|
||||
public class CreateEnvelopeCommandHandler(IServiceProvider Provider, IMapper Mapper, IEnvelopeNotificationService EnvNtfService, IRequestContext ReqCntxt, ILogger<CreateEnvelopeCommandHandler> Logger) : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?>
|
||||
{
|
||||
private readonly IServiceProvider _provider;
|
||||
private IEnvelopeExecutor Executor => Provider.GetRequiredService<IEnvelopeExecutor>();
|
||||
|
||||
private IEnvelopeExecutor Executor => _provider.GetRequiredService<IEnvelopeExecutor>();
|
||||
private IRepository<Envelope> Repository => Provider.GetRequiredService<IRepository<Envelope>>();
|
||||
|
||||
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
||||
private IRepository<Document> DocumentRepository => Provider.GetRequiredService<IRepository<Document>>();
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => Provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public CreateEnvelopeCommandHandler(IServiceProvider provider, IMapper mapper)
|
||||
{
|
||||
_provider = provider;
|
||||
_mapper = mapper;
|
||||
}
|
||||
private IRepository<EnvelopeType> EnvelopeTypeRepository => Provider.GetRequiredService<IRepository<EnvelopeType>>();
|
||||
|
||||
private IRepository<Receiver> ReceiverRepository => Provider.GetRequiredService<IRepository<Receiver>>();
|
||||
|
||||
private IRepository<DocReceiverElement> DocReceiverElementRepository => Provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
||||
|
||||
private IRepository<EmailTemplate> EmailTemplateRepository => Provider.GetRequiredService<IRepository<EmailTemplate>>();
|
||||
|
||||
private IRepository<EmailOut> EmailOutRepository => Provider.GetRequiredService<IRepository<EmailOut>>();
|
||||
|
||||
private MailParams MailParams => Provider.GetRequiredService<IOptions<MailParams>>().Value;
|
||||
|
||||
private DispatcherParams DispatcherParams => Provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
||||
|
||||
private SignatureFieldParams SignatureFieldParams => Provider.GetRequiredService<IOptionsMonitor<SignatureFieldParams>>().CurrentValue;
|
||||
|
||||
[Obsolete("Use IConfigService instead.")]
|
||||
private IConfigService ConfigService => Provider.GetRequiredService<IConfigService>();
|
||||
|
||||
private IMediator Mediator => Provider.GetRequiredService<IMediator>();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -40,10 +70,541 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
||||
/// <returns></returns>
|
||||
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
||||
{
|
||||
var envelope = request.UseSQLExecutor
|
||||
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel)
|
||||
: await Repository.CreateAsync(request, cancel);
|
||||
var now = DateTime.Now;
|
||||
var title = request.Title?.Trim() ?? string.Empty;
|
||||
var message = request.Message?.Trim() ?? string.Empty;
|
||||
var settings = await ResolveEnvelopeSettingsAsync(request, existing: null, cancel);
|
||||
|
||||
return _mapper.Map<EnvelopeDto>(envelope);
|
||||
Envelope envelope;
|
||||
if (request.EnvelopeId is int envelopeId)
|
||||
{
|
||||
envelope = await Repository.Query
|
||||
.Include(e => e.User)
|
||||
.FirstOrDefaultAsync(e => e.Id == envelopeId, cancel)
|
||||
?? throw new BadRequestException($"Envelope {envelopeId} not found.");
|
||||
|
||||
if (envelope.UserId != request.UserId)
|
||||
throw new ForbiddenException();
|
||||
|
||||
settings = await ResolveEnvelopeSettingsAsync(request, envelope, cancel);
|
||||
|
||||
await Repository.UpdateAsync(e =>
|
||||
{
|
||||
e.Title = title;
|
||||
e.Message = message;
|
||||
e.TfaEnabled = request.TFAEnabled;
|
||||
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||
e.Language = settings.Language;
|
||||
e.UseAccessCode = settings.UseAccessCode;
|
||||
e.CertificationType = settings.CertificationType;
|
||||
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||
e.SendReminderEmails = settings.SendReminderEmails;
|
||||
e.FirstReminderDays = settings.FirstReminderDays;
|
||||
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||
e.ContractType = settings.ContractType;
|
||||
e.ExpiresWhen = settings.ExpiresWhen;
|
||||
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||
e.ChangedWhen = now;
|
||||
}, e => e.Id == envelopeId, cancel);
|
||||
|
||||
envelope.Title = title;
|
||||
envelope.Message = message;
|
||||
envelope.TfaEnabled = request.TFAEnabled;
|
||||
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||
envelope.Language = settings.Language;
|
||||
envelope.UseAccessCode = settings.UseAccessCode;
|
||||
envelope.CertificationType = settings.CertificationType;
|
||||
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||
envelope.ContractType = settings.ContractType;
|
||||
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||
envelope.ChangedWhen = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
envelope = request.UseSQLExecutor
|
||||
? await Executor.CreateEnvelopeAsync(request.UserId, title, message, request.TFAEnabled, cancel)
|
||||
: await Repository.CreateAsync(new Envelope
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Status = EnvelopeStatus.EnvelopeCreated,
|
||||
Uuid = Guid.NewGuid().ToString(),
|
||||
Title = title,
|
||||
Message = message,
|
||||
AddedWhen = now,
|
||||
ChangedWhen = now,
|
||||
Comment = string.Empty,
|
||||
EnvelopeTypeId = settings.EnvelopeTypeId,
|
||||
Language = settings.Language,
|
||||
TfaEnabled = request.TFAEnabled,
|
||||
UseAccessCode = settings.UseAccessCode,
|
||||
CertificationType = settings.CertificationType,
|
||||
FinalEmailToCreator = settings.FinalEmailToCreator,
|
||||
FinalEmailToReceivers = settings.FinalEmailToReceivers,
|
||||
SendReminderEmails = settings.SendReminderEmails,
|
||||
FirstReminderDays = settings.FirstReminderDays,
|
||||
ReminderIntervalDays = settings.ReminderIntervalDays,
|
||||
ExpiresWhenDays = settings.ExpiresWhenDays,
|
||||
ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays,
|
||||
ExpiresWhen = settings.ExpiresWhen,
|
||||
ExpiresWarningWhen = settings.ExpiresWarningWhen,
|
||||
ContractType = settings.ContractType
|
||||
}, cancel);
|
||||
|
||||
if (request.UseSQLExecutor)
|
||||
{
|
||||
await Repository.UpdateAsync(e =>
|
||||
{
|
||||
e.Title = title;
|
||||
e.Message = message;
|
||||
e.TfaEnabled = request.TFAEnabled;
|
||||
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||
e.Language = settings.Language;
|
||||
e.UseAccessCode = settings.UseAccessCode;
|
||||
e.CertificationType = settings.CertificationType;
|
||||
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||
e.SendReminderEmails = settings.SendReminderEmails;
|
||||
e.FirstReminderDays = settings.FirstReminderDays;
|
||||
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||
e.ExpiresWhen = settings.ExpiresWhen;
|
||||
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||
e.ContractType = settings.ContractType;
|
||||
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||
e.ChangedWhen = now;
|
||||
}, e => e.Id == envelope.Id, cancel);
|
||||
|
||||
envelope.Title = title;
|
||||
envelope.Message = message;
|
||||
envelope.TfaEnabled = request.TFAEnabled;
|
||||
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||
envelope.Language = settings.Language;
|
||||
envelope.UseAccessCode = settings.UseAccessCode;
|
||||
envelope.CertificationType = settings.CertificationType;
|
||||
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||
envelope.ContractType = settings.ContractType;
|
||||
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||
envelope.ChangedWhen = now;
|
||||
}
|
||||
|
||||
await Mediator.Send(new CreateHistoryCommand
|
||||
{
|
||||
EnvelopeId = envelope.Id,
|
||||
UserReference = await ResolveSenderReferenceAsync(envelope, cancel),
|
||||
Status = EnvelopeStatus.EnvelopeCreated
|
||||
}, cancel);
|
||||
}
|
||||
|
||||
await UpsertDocumentAsync(envelope.Id, request.Document, now, cancel);
|
||||
await UpsertReceiversAndFieldsAsync(envelope.Id, request.Receivers, now, cancel);
|
||||
|
||||
if (request.Send)
|
||||
await SendEnvelopeAsync(envelope, cancel);
|
||||
|
||||
envelope = await Repository.Query
|
||||
.Include(e => e.User)
|
||||
.Include(e => e.EnvelopeReceivers)
|
||||
.ThenInclude(er => er.Receiver)
|
||||
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
||||
|
||||
return Mapper.Map<EnvelopeDto>(envelope);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
||||
{
|
||||
EnvelopeType? selectedType = null;
|
||||
if (request.EnvelopeTypeId is int envelopeTypeId && envelopeTypeId > 0)
|
||||
{
|
||||
selectedType = await EnvelopeTypeRepository.Query
|
||||
.FirstOrDefaultAsync(t => t.Id == envelopeTypeId, cancel)
|
||||
?? throw new BadRequestException($"Envelope type {envelopeTypeId} not found.");
|
||||
}
|
||||
|
||||
var expiresWhenDays = Math.Max(0,
|
||||
request.ExpiresWhenDays
|
||||
?? selectedType?.ExpiresWhenDays
|
||||
?? existing?.ExpiresWhenDays
|
||||
?? 0);
|
||||
|
||||
var expiresWarningWhenDays = Math.Max(0,
|
||||
request.ExpiresWarningWhenDays
|
||||
?? selectedType?.ExpiresWarningWhenDays
|
||||
?? existing?.ExpiresWarningWhenDays
|
||||
?? 0);
|
||||
|
||||
var useAccessCode = request.UseAccessCode
|
||||
?? selectedType?.UseAccessCode
|
||||
?? existing?.UseAccessCode
|
||||
?? false;
|
||||
|
||||
if (request.TFAEnabled)
|
||||
useAccessCode = true;
|
||||
|
||||
return new EnvelopeSettings(
|
||||
EnvelopeTypeId: request.EnvelopeTypeId ?? existing?.EnvelopeTypeId,
|
||||
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
||||
UseAccessCode: useAccessCode,
|
||||
CertificationType: request.CertificationType
|
||||
?? selectedType?.CertificationType
|
||||
?? existing?.CertificationType
|
||||
?? CertificationType.AdvancedElectronicSignature,
|
||||
FinalEmailToCreator: request.FinalEmailToCreator
|
||||
?? (int?)selectedType?.FinalEmailToCreator
|
||||
?? existing?.FinalEmailToCreator
|
||||
?? (int)FinalEmailType.No,
|
||||
FinalEmailToReceivers: request.FinalEmailToReceivers
|
||||
?? (int?)selectedType?.FinalEmailToReceivers
|
||||
?? existing?.FinalEmailToReceivers
|
||||
?? (int)FinalEmailType.No,
|
||||
SendReminderEmails: request.SendReminderEmails
|
||||
?? selectedType?.SendReminderEmails
|
||||
?? existing?.SendReminderEmails
|
||||
?? false,
|
||||
FirstReminderDays: Math.Max(0,
|
||||
request.FirstReminderDays
|
||||
?? selectedType?.FirstReminderDays
|
||||
?? existing?.FirstReminderDays
|
||||
?? 0),
|
||||
ReminderIntervalDays: Math.Max(0,
|
||||
request.ReminderIntervalDays
|
||||
?? selectedType?.ReminderIntervalDays
|
||||
?? existing?.ReminderIntervalDays
|
||||
?? 0),
|
||||
ExpiresWhenDays: expiresWhenDays,
|
||||
ExpiresWarningWhenDays: expiresWarningWhenDays,
|
||||
ExpiresWhen: expiresWhenDays > 0 ? DateTime.Now.AddDays(expiresWhenDays) : null,
|
||||
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
||||
ContractType: existing?.ContractType
|
||||
?? selectedType?.ContractType
|
||||
?? ContractType.Contract);
|
||||
}
|
||||
|
||||
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(preferred))
|
||||
return preferred.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fallback1))
|
||||
return fallback1.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fallback2))
|
||||
return fallback2.Trim();
|
||||
|
||||
return "de-DE";
|
||||
}
|
||||
|
||||
private sealed record EnvelopeSettings(
|
||||
int? EnvelopeTypeId,
|
||||
string Language,
|
||||
bool UseAccessCode,
|
||||
CertificationType CertificationType,
|
||||
int FinalEmailToCreator,
|
||||
int FinalEmailToReceivers,
|
||||
bool SendReminderEmails,
|
||||
int FirstReminderDays,
|
||||
int ReminderIntervalDays,
|
||||
int ExpiresWhenDays,
|
||||
int ExpiresWarningWhenDays,
|
||||
DateTime? ExpiresWhen,
|
||||
DateTime? ExpiresWarningWhen,
|
||||
ContractType ContractType);
|
||||
|
||||
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
|
||||
return envelope.User.Email;
|
||||
|
||||
var senderEmail = await Repository.Query
|
||||
.Where(e => e.Id == envelope.Id)
|
||||
.Select(e => e.User.Email)
|
||||
.FirstOrDefaultAsync(cancel);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(senderEmail))
|
||||
throw new BadRequestException("Envelope sender email is missing.");
|
||||
|
||||
return senderEmail;
|
||||
}
|
||||
|
||||
private async Task UpsertDocumentAsync(int envelopeId, CreateEnvelopeDocumentDto? document, DateTime now, CancellationToken cancel)
|
||||
{
|
||||
if (document is null || string.IsNullOrWhiteSpace(document.DataAsBase64))
|
||||
return;
|
||||
|
||||
byte[] bytes;
|
||||
try
|
||||
{
|
||||
bytes = Convert.FromBase64String(document.DataAsBase64);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new BadRequestException("Document payload is not valid base64.");
|
||||
}
|
||||
|
||||
var existingDocumentId = await DocumentRepository.Query
|
||||
.Where(d => d.EnvelopeId == envelopeId)
|
||||
.Select(d => (int?)d.Id)
|
||||
.FirstOrDefaultAsync(cancel);
|
||||
|
||||
if (existingDocumentId is null)
|
||||
{
|
||||
await DocumentRepository.CreateAsync(new Document
|
||||
{
|
||||
EnvelopeId = envelopeId,
|
||||
AddedWhen = now,
|
||||
ByteData = bytes,
|
||||
Filename = string.Empty,
|
||||
Filepath = string.Empty,
|
||||
FileNameOriginal = string.Empty
|
||||
}, cancel);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await DocumentRepository.UpdateAsync(d =>
|
||||
{
|
||||
d.ByteData = bytes;
|
||||
}, d => d.Id == existingDocumentId.Value, cancel);
|
||||
}
|
||||
|
||||
private async Task UpsertReceiversAndFieldsAsync(int envelopeId, IReadOnlyCollection<CreateEnvelopeReceiverDto> receivers, DateTime now, CancellationToken cancel)
|
||||
{
|
||||
var filteredReceivers = receivers
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.EmailAddress))
|
||||
.Select(r => r with
|
||||
{
|
||||
EmailAddress = r.EmailAddress.Trim().ToLowerInvariant(),
|
||||
Name = r.Name?.Trim(),
|
||||
PhoneNumber = string.IsNullOrWhiteSpace(r.PhoneNumber) ? null : r.PhoneNumber.Trim(),
|
||||
Fields = r.Fields ?? new List<CreateEnvelopeFieldDto>()
|
||||
})
|
||||
.GroupBy(r => r.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
if (filteredReceivers.Count == 0)
|
||||
return;
|
||||
|
||||
var document = await DocumentRepository.Query
|
||||
.FirstOrDefaultAsync(d => d.EnvelopeId == envelopeId, cancel);
|
||||
|
||||
if (document is null)
|
||||
return;
|
||||
|
||||
var currentLinks = await EnvelopeReceiverRepository.Query
|
||||
.Include(er => er.Receiver)
|
||||
.Where(er => er.EnvelopeId == envelopeId)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
var linksByEmail = currentLinks
|
||||
.Where(er => !string.IsNullOrWhiteSpace(er.Receiver?.EmailAddress))
|
||||
.GroupBy(er => er.Receiver!.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var sequence = currentLinks.Count > 0 ? currentLinks.Max(er => er.Sequence) : 0;
|
||||
|
||||
foreach (var receiverDraft in filteredReceivers)
|
||||
{
|
||||
var linkResult = await GetOrCreateEnvelopeReceiverAsync(envelopeId, receiverDraft, linksByEmail, now, sequence, cancel);
|
||||
var link = linkResult.Link;
|
||||
sequence = linkResult.NextSequence;
|
||||
|
||||
await DocReceiverElementRepository.DeleteAsync(query => query
|
||||
.Where(e => e.DocumentId == document.Id)
|
||||
.Where(e => e.ReceiverId == link.ReceiverId), cancel);
|
||||
|
||||
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
|
||||
{
|
||||
await DocReceiverElementRepository.CreateAsync(new DocReceiverElement
|
||||
{
|
||||
DocumentId = document.Id,
|
||||
ReceiverId = link.ReceiverId,
|
||||
ElementType = 1,
|
||||
X = field.X,
|
||||
Y = field.Y,
|
||||
Width = SignatureFieldParams.Width,
|
||||
Height = SignatureFieldParams.Height,
|
||||
Page = field.Page,
|
||||
Required = true,
|
||||
ReadOnly = false,
|
||||
AnnotationIndex = 0,
|
||||
AddedWhen = now,
|
||||
FullName = link.Name,
|
||||
Position = string.Empty,
|
||||
Place = string.Empty,
|
||||
Tooltip = string.Empty
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(EnvelopeReceiver Link, int NextSequence)> GetOrCreateEnvelopeReceiverAsync(
|
||||
int envelopeId,
|
||||
CreateEnvelopeReceiverDto receiverDraft,
|
||||
IDictionary<string, EnvelopeReceiver> linksByEmail,
|
||||
DateTime now,
|
||||
int sequence,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
if (linksByEmail.TryGetValue(receiverDraft.EmailAddress, out var existingLink))
|
||||
{
|
||||
var nextName = string.IsNullOrWhiteSpace(receiverDraft.Name) ? existingLink.Name : receiverDraft.Name!;
|
||||
var nextPhoneNumber = receiverDraft.PhoneNumber ?? existingLink.PhoneNumber;
|
||||
|
||||
await EnvelopeReceiverRepository.UpdateAsync(er =>
|
||||
{
|
||||
er.Name = nextName;
|
||||
er.PhoneNumber = nextPhoneNumber;
|
||||
er.ChangedWhen = now;
|
||||
}, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel);
|
||||
|
||||
existingLink.Name = nextName;
|
||||
existingLink.PhoneNumber = nextPhoneNumber;
|
||||
return (existingLink, sequence);
|
||||
}
|
||||
|
||||
var receiver = await ReceiverRepository.Query
|
||||
.FirstOrDefaultAsync(r => r.EmailAddress != null && r.EmailAddress.ToLower() == receiverDraft.EmailAddress, cancel);
|
||||
|
||||
if (receiver is null)
|
||||
{
|
||||
receiver = await ReceiverRepository.CreateAsync(new Receiver
|
||||
{
|
||||
EmailAddress = receiverDraft.EmailAddress,
|
||||
Signature = receiverDraft.EmailAddress.ToUpperInvariant().GetChecksum(),
|
||||
AddedWhen = now,
|
||||
TotpSecretkey = string.Empty,
|
||||
TfaRegDeadline = null
|
||||
}, cancel);
|
||||
}
|
||||
|
||||
sequence++;
|
||||
var link = await EnvelopeReceiverRepository.CreateAsync(new EnvelopeReceiver
|
||||
{
|
||||
EnvelopeId = envelopeId,
|
||||
ReceiverId = receiver.Id,
|
||||
Sequence = sequence,
|
||||
Name = receiverDraft.Name ?? receiverDraft.EmailAddress,
|
||||
JobTitle = string.Empty,
|
||||
CompanyName = string.Empty,
|
||||
PrivateMessage = string.Empty,
|
||||
AccessCode = string.Empty,
|
||||
AddedWhen = now,
|
||||
ChangedWhen = now,
|
||||
PhoneNumber = receiverDraft.PhoneNumber ?? string.Empty
|
||||
}, cancel);
|
||||
|
||||
link.Receiver = receiver;
|
||||
linksByEmail[receiverDraft.EmailAddress] = link;
|
||||
return (link, sequence);
|
||||
}
|
||||
|
||||
private async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancel)
|
||||
{
|
||||
var hasDocument = await DocumentRepository.Query
|
||||
.AnyAsync(d => d.EnvelopeId == envelope.Id && d.ByteData != null && d.ByteData.Length > 0, cancel);
|
||||
|
||||
if (!hasDocument)
|
||||
throw new BadRequestException("Cannot send envelope without a document.");
|
||||
|
||||
var envRcvs = await EnvelopeReceiverRepository.Query
|
||||
.Include(er => er.Receiver)
|
||||
.Where(er => er.EnvelopeId == envelope.Id)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
if (envRcvs.Count == 0)
|
||||
throw new BadRequestException("Cannot send envelope without receivers.");
|
||||
|
||||
var receiverIds = envRcvs.Select(l => l.ReceiverId).Distinct().ToList();
|
||||
var documentIds = await DocumentRepository.Query
|
||||
.Where(d => d.EnvelopeId == envelope.Id)
|
||||
.Select(d => d.Id)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
var receiverFieldCounts = await DocReceiverElementRepository.Query
|
||||
.Where(e => documentIds.Contains(e.DocumentId))
|
||||
.Where(e => receiverIds.Contains(e.ReceiverId))
|
||||
.GroupBy(e => e.ReceiverId)
|
||||
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
||||
|
||||
var receiversWithoutFields = envRcvs
|
||||
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
||||
.Select(link => link.Name)
|
||||
.ToList();
|
||||
|
||||
if (receiversWithoutFields.Count > 0)
|
||||
throw new BadRequestException("Cannot send envelope because some receivers have no signature fields.");
|
||||
|
||||
var changedWhen = DateTime.Now;
|
||||
await Repository.UpdateAsync(e =>
|
||||
{
|
||||
e.Status = EnvelopeStatus.EnvelopeQueued;
|
||||
e.ChangedWhen = changedWhen;
|
||||
}, e => e.Id == envelope.Id, cancel);
|
||||
|
||||
var userReference = await ResolveSenderReferenceAsync(envelope, cancel);
|
||||
|
||||
await Mediator.Send(new CreateHistoryCommand
|
||||
{
|
||||
EnvelopeId = envelope.Id,
|
||||
UserReference = userReference,
|
||||
Status = EnvelopeStatus.EnvelopeQueued
|
||||
}, cancel);
|
||||
|
||||
await SendInvitationEmailsAsync(envelope, envRcvs, cancel);
|
||||
|
||||
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
||||
}
|
||||
|
||||
private async Task SendInvitationEmailsAsync(
|
||||
Envelope envelope,
|
||||
IReadOnlyCollection<EnvelopeReceiver> envRcvs,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
foreach (var er in envRcvs)
|
||||
{
|
||||
var advancedSignature = envelope.CertificationType == CertificationType.AdvancedElectronicSignature;
|
||||
var origin = new Uri(ReqCntxt.Origin ?? throw new InvalidOperationException($"The request origin is missing in {nameof(CreateEnvelopeCommandHandler)}."));
|
||||
try
|
||||
{
|
||||
await EnvNtfService.Notify(er.Receiver.EmailAddress, new DocumentReceivedNotification
|
||||
{
|
||||
AdvancedSignature = advancedSignature,
|
||||
DocumentTitle = envelope.Title ?? string.Empty,
|
||||
NameReceiver = er.Name ?? string.Empty,
|
||||
EnvelopeUuid = envelope.Uuid,
|
||||
IsReadAndConfirm = envelope.IsReadAndConfirm(),
|
||||
Message = envelope.Message ?? string.Empty,
|
||||
NameSender = $"{envelope.User.Prename} {envelope.User.Name}",
|
||||
Origin = origin,
|
||||
ReceiverSignature = er.Receiver.Signature,
|
||||
}, cancel);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to send invitation email for receiver {emailAddress} in envelope {envelopeId}.", er.Receiver.EmailAddress, envelope.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,89 @@
|
||||
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Histories.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
|
||||
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
||||
{
|
||||
private readonly IRepository<Envelope> _envelopeRepository;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public DeleteEnvelopeCommandHandler(
|
||||
IRepository<Envelope> envelopeRepository,
|
||||
IMediator mediator)
|
||||
{
|
||||
_envelopeRepository = envelopeRepository;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
||||
{
|
||||
var reason = request.Reason?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
throw new BadRequestException("Please provide a reason.");
|
||||
|
||||
var envelope = await _envelopeRepository.Query
|
||||
.Include(e => e.User)
|
||||
.Include(e => e.EnvelopeReceivers)
|
||||
.ThenInclude(er => er.Receiver)
|
||||
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
||||
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
||||
|
||||
if (envelope.UserId != request.UserId)
|
||||
throw new ForbiddenException();
|
||||
|
||||
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
||||
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
||||
|
||||
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
||||
? EnvelopeStatus.EnvelopeWithdrawn
|
||||
: EnvelopeStatus.EnvelopeDeleted;
|
||||
|
||||
var changedWhen = DateTime.Now;
|
||||
envelope.Status = targetStatus;
|
||||
envelope.Comment = reason;
|
||||
envelope.ChangedWhen = changedWhen;
|
||||
|
||||
await _envelopeRepository.UpdateAsync(e =>
|
||||
{
|
||||
e.Status = targetStatus;
|
||||
e.Comment = reason;
|
||||
e.ChangedWhen = changedWhen;
|
||||
}, e => e.Id == envelope.Id, cancel);
|
||||
|
||||
var userReference = envelope.User?.Email;
|
||||
if (string.IsNullOrWhiteSpace(userReference))
|
||||
throw new BadRequestException("Envelope sender email is missing.");
|
||||
|
||||
await _mediator.Send(new CreateHistoryCommand
|
||||
{
|
||||
EnvelopeId = envelope.Id,
|
||||
UserReference = userReference,
|
||||
Status = targetStatus,
|
||||
Comment = reason
|
||||
}, cancel);
|
||||
|
||||
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||
{
|
||||
if (envelopeReceiver.Receiver is null)
|
||||
continue;
|
||||
|
||||
await _mediator.Publish(new EnvelopeDeletedNotification
|
||||
{
|
||||
Envelope = envelope,
|
||||
EnvelopeReceiver = envelopeReceiver,
|
||||
Reason = reason,
|
||||
SenderReference = userReference
|
||||
}, cancel);
|
||||
}
|
||||
|
||||
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using AutoMapper;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
@@ -14,6 +17,11 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
/// </summary>
|
||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool IncludeDocAndDocRes { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -148,10 +156,92 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
||||
if (request is { OnlyCompleted: true })
|
||||
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||
|
||||
if (!request.IncludeDocAndDocRes)
|
||||
{
|
||||
return await query
|
||||
.Select(e => new EnvelopeDto
|
||||
{
|
||||
Id = e.Id,
|
||||
UserId = e.UserId,
|
||||
Status = e.Status,
|
||||
StatusName = string.Empty,
|
||||
Uuid = e.Uuid,
|
||||
Message = e.Message,
|
||||
AddedWhen = e.AddedWhen,
|
||||
ChangedWhen = e.ChangedWhen,
|
||||
Title = e.Title ?? string.Empty,
|
||||
ContractType = e.ContractType,
|
||||
Language = e.Language,
|
||||
EnvelopeTypeId = e.EnvelopeTypeId,
|
||||
CertificationType = e.CertificationType,
|
||||
FinalEmailToCreator = e.FinalEmailToCreator,
|
||||
FinalEmailToReceivers = e.FinalEmailToReceivers,
|
||||
SendReminderEmails = e.SendReminderEmails,
|
||||
FirstReminderDays = e.FirstReminderDays,
|
||||
ReminderIntervalDays = e.ReminderIntervalDays,
|
||||
ExpiresWhenDays = e.ExpiresWhenDays,
|
||||
ExpiresWarningWhenDays = e.ExpiresWarningWhenDays,
|
||||
UseAccessCode = e.UseAccessCode ?? true,
|
||||
TFAEnabled = e.TfaEnabled ?? false,
|
||||
EnvelopeTypeTitle = e.Type != null ? e.Type.Title : null,
|
||||
IsAlreadySent = false,
|
||||
DocResult = null,
|
||||
Documents = null,
|
||||
EnvelopeReceivers = e.EnvelopeReceivers.Select(er => new EnvelopeReceiverDto
|
||||
{
|
||||
EnvelopeId = er.EnvelopeId,
|
||||
ReceiverId = er.ReceiverId,
|
||||
Sequence = er.Sequence,
|
||||
Name = er.Name,
|
||||
JobTitle = er.JobTitle,
|
||||
CompanyName = er.CompanyName,
|
||||
PrivateMessage = er.PrivateMessage,
|
||||
AccessCode = er.AccessCode,
|
||||
AddedWhen = er.AddedWhen,
|
||||
ChangedWhen = er.ChangedWhen,
|
||||
HasPhoneNumber = er.HasPhoneNumber,
|
||||
Receiver = er.Receiver == null
|
||||
? null
|
||||
: new ReceiverDto
|
||||
{
|
||||
Id = er.Receiver.Id,
|
||||
EmailAddress = er.Receiver.EmailAddress,
|
||||
Signature = er.Receiver.Signature,
|
||||
AddedWhen = er.Receiver.AddedWhen,
|
||||
TfaRegDeadline = er.Receiver.TfaRegDeadline
|
||||
}
|
||||
}),
|
||||
Histories = e.Histories.Select(h => new HistoryDto
|
||||
{
|
||||
Id = h.Id,
|
||||
EnvelopeId = h.EnvelopeId,
|
||||
UserReference = h.UserReference,
|
||||
Status = h.Status,
|
||||
StatusName = null,
|
||||
AddedWhen = h.AddedWhen,
|
||||
ActionDate = h.ActionDate,
|
||||
Comment = h.Comment,
|
||||
Receiver = h.Receiver == null
|
||||
? null
|
||||
: new ReceiverDto
|
||||
{
|
||||
Id = h.Receiver.Id,
|
||||
EmailAddress = h.Receiver.EmailAddress,
|
||||
Signature = h.Receiver.Signature,
|
||||
AddedWhen = h.Receiver.AddedWhen,
|
||||
TfaRegDeadline = h.Receiver.TfaRegDeadline
|
||||
}
|
||||
})
|
||||
})
|
||||
.ToListAsync(cancel);
|
||||
}
|
||||
|
||||
var envelopes = await query
|
||||
.Include(e => e.Type)
|
||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||
.Include(e => e.Histories)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record ReadSignedEnvelopeQuery : IRequest<IEnumerable<EnvelopeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool? Burned { get; init; } = null;
|
||||
|
||||
internal int? UserId { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public ReadSignedEnvelopeQuery Authorized(int userId)
|
||||
{
|
||||
UserId = userId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Repo"></param>
|
||||
/// <param name="Mapper"></param>
|
||||
public class ReadSignedEnvelopeQueryHandler(IRepository<Envelope> Repo, IMapper Mapper) : IRequestHandler<ReadSignedEnvelopeQuery, IEnumerable<EnvelopeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public async Task<IEnumerable<EnvelopeDto>> Handle(ReadSignedEnvelopeQuery request, CancellationToken cancel)
|
||||
{
|
||||
var eQuery = Repo.Query.Where(e => e.Status == Domain.Constants.EnvelopeStatus.EnvelopeCompletelySigned);
|
||||
|
||||
if (request.Burned is bool burned)
|
||||
eQuery = burned ? eQuery.Where(e => e.DocResult != null) : eQuery.Where(e => e.DocResult == null);
|
||||
|
||||
if(request.UserId is int userId)
|
||||
eQuery = eQuery.Where(e => e.UserId == userId);
|
||||
|
||||
var envelopes = await eQuery.ToListAsync(cancel);
|
||||
|
||||
return Mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.PdfSigning;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record FinalizeEnvelopeCommand : EnvelopeQueryBase, IRequest
|
||||
{
|
||||
internal int? UserId { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public FinalizeEnvelopeCommand Authorized(int userId)
|
||||
{
|
||||
UserId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool BurnedAllowed { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool SaveDocResult { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Repo"></param>
|
||||
/// <param name="Sender"></param>
|
||||
/// <param name="NtfServer"></param>
|
||||
public class FinalizeEnvelopeCommandHandler(IRepository<Envelope> Repo, ISender Sender, IEnvelopeNotificationService NtfServer) : IRequestHandler<FinalizeEnvelopeCommand>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public async Task Handle(FinalizeEnvelopeCommand request, CancellationToken cancel)
|
||||
{
|
||||
var envelopeQuery = Repo.Query;
|
||||
|
||||
if (request.Id is int id)
|
||||
envelopeQuery = envelopeQuery.Where(e => e.Id == id);
|
||||
else if (request.Uuid is string uuid)
|
||||
envelopeQuery = envelopeQuery.Where(e => e.Uuid == uuid);
|
||||
|
||||
var envelope = await envelopeQuery
|
||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||
.Include(e => e.Documents)
|
||||
.SingleOrDefaultAsync(cancel)
|
||||
?? throw new NotFoundException($"Envelope is not found ID: {request.Id} UUID: {request.Uuid}");
|
||||
|
||||
if(request.UserId is int userId && userId != envelope.UserId)
|
||||
throw new InvalidOperationException($"Envelope is not owned by user ID: {request.Id} UUID: {request.Uuid}");
|
||||
|
||||
// already burned
|
||||
if (!request.BurnedAllowed && envelope.IsBurned())
|
||||
throw new InvalidOperationException($"Envelope is already burned ID: {request.Id} UUID: {request.Uuid}");
|
||||
|
||||
// envelope is not completely signed
|
||||
if (envelope.Status!= EnvelopeStatus.EnvelopeCompletelySigned)
|
||||
throw new InvalidOperationException($"Envelope status is not completely signed ID: {request.Id} UUID: {request.Uuid}");
|
||||
|
||||
using var docStream = await Sender.Send(new SignPdfByEnvelopeCommand()
|
||||
{
|
||||
Envelope = envelope
|
||||
}, cancel);
|
||||
|
||||
if (docStream.CanSeek)
|
||||
docStream.Position = 0;
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
docStream.CopyTo(ms);
|
||||
byte[] doc = ms.ToArray();
|
||||
|
||||
foreach (var er in envelope.EnvelopeReceivers)
|
||||
{
|
||||
await NtfServer.Notify(er.Receiver.EmailAddress, new DocumentSignedNotification()
|
||||
{
|
||||
DocumentTitle = envelope.Title,
|
||||
NameReceiver = er.Name,
|
||||
Attachments = [($"unterzeichnetes-dokument.pdf", doc)],
|
||||
IsReadAndConfirm = envelope.IsReadAndConfirm(),
|
||||
}, cancel);
|
||||
}
|
||||
|
||||
if (request.SaveDocResult)
|
||||
await Repo.UpdateAsync(e => e.DocResult = doc, e => e.Id == envelope.Id, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Enums;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.DocReceiverElements.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Application.PdfSigning;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record SignPdfByElementCommand : IRequest<Stream>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DocReceiverElementDto? Element { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? ElementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom document
|
||||
/// </summary>
|
||||
public Stream? Document { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public PdfSignatureCertificationLevel CertificationLevel { get; init; } = PdfSignatureCertificationLevel.NoChangesAllowed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="signService"></param>
|
||||
/// <param name="imgRenderer"></param>
|
||||
/// <param name="options"></param>
|
||||
public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService signService, IPdfPageImageRenderer imgRenderer, IOptionsMonitor<ESignParams> options) : IRequestHandler<SignPdfByElementCommand, Stream>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Stream> Handle(SignPdfByElementCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// ensure element
|
||||
if (request.Element is null)
|
||||
{
|
||||
var elements = await sender.Send(new ReadDocReceiverElementQuery()
|
||||
{
|
||||
Id = request.ElementId!,
|
||||
IncludeDocument = request.Document is null
|
||||
}, cancellationToken);
|
||||
|
||||
request.Element = elements.FirstOrDefault() ?? throw new NotFoundException($"Element with ID {request.ElementId} not found."); ;
|
||||
}
|
||||
|
||||
// ensure document
|
||||
if(request.Document is null)
|
||||
{
|
||||
if (request.Element?.Document?.ByteData is not { Length: > 0 })
|
||||
throw new NotFoundException("Document data is missing or empty.");
|
||||
|
||||
request.Document = new MemoryStream(request.Element.Document.ByteData);
|
||||
}
|
||||
|
||||
ValidateAfterDataIsComplete(request);
|
||||
|
||||
// create signature image from element
|
||||
var img = imgRenderer.ComposeSignaturePreviewPng(request.Element);
|
||||
|
||||
// sign pdf
|
||||
var outputPdf = new MemoryStream();
|
||||
|
||||
await signService.SignPdfAsync(
|
||||
inputPdf: request.Document,
|
||||
outputPdf: outputPdf,
|
||||
options: new PdfSignOptionsDto
|
||||
{
|
||||
Name = request.Element.FullName!,
|
||||
Location = request.Element.Position!,
|
||||
Phone = request.Phone,
|
||||
Reason = request.Reason ?? options.CurrentValue.DefaultReason,
|
||||
Email = request.Element.Receiver!.EmailAddress,
|
||||
ApplicationName = options.CurrentValue.ApplicationName,
|
||||
DocumentElementId = request.Element.Id,
|
||||
|
||||
CertificationLevel = request.CertificationLevel,
|
||||
Appearance = new PdfSignatureAppearanceDto(
|
||||
PageNumber: request.Element.Page,
|
||||
Left: request.Element.Left,
|
||||
Bottom: request.Element.Y,
|
||||
Width: request.Element.Width,
|
||||
Height: request.Element.Height,
|
||||
CoordinateUnit: PdfSignatureCoordinateUnit.Inch,
|
||||
CoordinateOrigin: PdfSignatureCoordinateOrigin.TopLeft)
|
||||
},
|
||||
image: img,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return outputPdf;
|
||||
}
|
||||
|
||||
private static void ValidateAfterDataIsComplete(SignPdfByElementCommand request)
|
||||
{
|
||||
if(request.Element!.FullName is null)
|
||||
throw new BadRequestException($"Document is not signed. FullName is required. Element ID: {request.Element.Id}");
|
||||
else if(request.Element.Position is null)
|
||||
throw new BadRequestException($"Document is not signed. Position is required. Element ID: {request.Element.Id}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace EnvelopeGenerator.Application.PdfSigning;
|
||||
|
||||
public class SignPdfByElementCommandValidator : AbstractValidator<SignPdfByElementCommand>
|
||||
{
|
||||
public SignPdfByElementCommandValidator()
|
||||
{
|
||||
RuleFor(x => x)
|
||||
.Must(x => (x.Element is not null) ^ x.ElementId.HasValue)
|
||||
.WithMessage("Exactly one of Element or ElementId must be provided.");
|
||||
|
||||
RuleFor(x => x.ElementId)
|
||||
.GreaterThan(0)
|
||||
.When(x => x.ElementId.HasValue)
|
||||
.WithMessage("ElementId must be greater than 0.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.PdfSigning;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record SignPdfByEnvelopeCommand : EnvelopeQueryBase, IRequest<Stream>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? ReceiverId { get; init; } = null;
|
||||
|
||||
internal int? UserId { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public SignPdfByEnvelopeCommand Authorized(int userId)
|
||||
{
|
||||
UserId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
internal Envelope? Envelope { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class SignPdfByEnvelopeCommandHandler(IRepository<Envelope> Repo, ISender sender) : IRequestHandler<SignPdfByEnvelopeCommand, Stream>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public async Task<Stream> Handle(SignPdfByEnvelopeCommand request, CancellationToken cancel)
|
||||
{
|
||||
if (request.Envelope is null)
|
||||
{
|
||||
var envelopeQuery = Repo.Query;
|
||||
|
||||
if (request.Id is int id)
|
||||
envelopeQuery = envelopeQuery.Where(e => e.Id == id);
|
||||
else if (request.Uuid is string uuid)
|
||||
envelopeQuery = envelopeQuery.Where(e => e.Uuid == uuid);
|
||||
|
||||
request.Envelope = await envelopeQuery.Include(e => e.EnvelopeReceivers).Include(e => e.Documents).SingleOrDefaultAsync(cancel)
|
||||
?? throw new NotFoundException($"Envelope is not found ID: {request.Id} UUID: {request.Uuid}");
|
||||
}
|
||||
|
||||
var envelope = request.Envelope;
|
||||
|
||||
if (request.UserId is int userId && userId != envelope.UserId)
|
||||
throw new AuthorizationException($"Envelope is not authorized for user with Id: {request.UserId} Envelope ID: {request.Id} UUID: {request.Uuid}");
|
||||
|
||||
if (envelope.EnvelopeReceivers.Count == 0)
|
||||
throw new NotFoundException($"Envelope has no receivers to sign ID: {request.Id} UUID: {request.Uuid}");
|
||||
|
||||
if(envelope.Documents.FirstOrDefault() is not Document document || document.ByteData is not byte[] byteData || byteData.Length == 0)
|
||||
throw new NotFoundException($"Envelope has no documents to sign ID: {request.Id} UUID: {request.Uuid}");
|
||||
|
||||
Stream doc = new MemoryStream(byteData);
|
||||
|
||||
var eReceivers = request.ReceiverId is int receiverId
|
||||
? envelope.EnvelopeReceivers.Where(e => e.ReceiverId == receiverId)
|
||||
: envelope.EnvelopeReceivers;
|
||||
|
||||
foreach (var eReceiver in eReceivers)
|
||||
{
|
||||
var signedDoc = await sender.Send(new SignPdfByEnvelopeReceiverCommand()
|
||||
{
|
||||
EnvelopeId = eReceiver.EnvelopeId,
|
||||
ReceiverId = eReceiver.ReceiverId
|
||||
}, cancel);
|
||||
|
||||
await doc.DisposeAsync();
|
||||
doc = signedDoc;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Enums;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.PdfSigning;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record SignPdfByEnvelopeReceiverCommand : IRequest<Stream>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int ReceiverId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public Stream? Document { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class SignPdfByEnvelopeReceiverCommandHandler(IRepository<DocReceiverElement> Repo, IMapper Mapper, ISender Sender) : IRequestHandler<SignPdfByEnvelopeReceiverCommand, Stream>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public async Task<Stream> Handle(SignPdfByEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||
{
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
// read elements
|
||||
var elements = await Repo.Where(el => el.ReceiverId == request.ReceiverId && el.Document.EnvelopeId == request.EnvelopeId)
|
||||
.Include(e => e.Receiver)
|
||||
.ToListAsync(cancel) ?? throw new NotFoundException($"No elements found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}.");
|
||||
var elementDtos = Mapper.Map<List<DocReceiverElementDto>>(elements)
|
||||
.OrderBy(e => e.Page)
|
||||
.ThenBy(e => e.Id)
|
||||
.ToList();
|
||||
|
||||
// read secrets for phone number
|
||||
var erSecrets = await Sender.Send(new ReadEnvelopeReceiverSecretQuery()
|
||||
{
|
||||
Envelope = new() { Id = request.EnvelopeId },
|
||||
Receiver = new() { Id = request.ReceiverId }
|
||||
}, cancel) ?? throw new DataIntegrityException($"No secrets found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}.");
|
||||
|
||||
// read document
|
||||
if(request.Document is null)
|
||||
{
|
||||
var unsignedDoc = await Sender.Send(new ReadDocumentQuery(EnvelopeId: request.EnvelopeId), cancel);
|
||||
var unsignedDocBytes = unsignedDoc.ByteData ?? throw new NotFoundException($"Document not found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}.");
|
||||
request.Document = new MemoryStream(unsignedDocBytes);
|
||||
}
|
||||
|
||||
Stream doc = request.Document;
|
||||
|
||||
// sign document for each element
|
||||
foreach (var element in elementDtos)
|
||||
{
|
||||
var lastElement = element == elementDtos.Last();
|
||||
|
||||
var signedDoc = await Sender.Send(new SignPdfByElementCommand
|
||||
{
|
||||
Document = doc,
|
||||
Element = element,
|
||||
CertificationLevel = lastElement ? PdfSignatureCertificationLevel.NoChangesAllowed : PdfSignatureCertificationLevel.FillFormsAndAnnotate,
|
||||
Phone = erSecrets.PhoneNumber
|
||||
}, cancel);
|
||||
|
||||
await doc.DisposeAsync();
|
||||
doc = signedDoc;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,9 @@
|
||||
<data name="Back" xml:space="preserve">
|
||||
<value>Zurück</value>
|
||||
</data>
|
||||
<data name="Next" xml:space="preserve">
|
||||
<value>Weiter</value>
|
||||
</data>
|
||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||
<value>Klicken Sie auf den</value>
|
||||
</data>
|
||||
@@ -564,6 +567,12 @@
|
||||
<data name="AddReceiver" xml:space="preserve">
|
||||
<value>Empfänger hinzufügen</value>
|
||||
</data>
|
||||
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
|
||||
<value>Zuletzt verwendete Empfänger</value>
|
||||
</data>
|
||||
<data name="Skip" xml:space="preserve">
|
||||
<value>Überspringen</value>
|
||||
</data>
|
||||
<data name="DeleteReceiver" xml:space="preserve">
|
||||
<value>Empfänger löschen</value>
|
||||
</data>
|
||||
@@ -651,4 +660,125 @@
|
||||
<data name="Status" xml:space="preserve">
|
||||
<value>Status</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="ChangedAt" xml:space="preserve"><value>Geändert am</value></data>
|
||||
<data name="Close" xml:space="preserve"><value>Schließen</value></data>
|
||||
<data name="CloseLanguageMenu" xml:space="preserve"><value>Sprachmenü schließen</value></data>
|
||||
<data name="ConfirmSignature" xml:space="preserve"><value>Unterschrift bestätigen</value></data>
|
||||
<data name="Created" xml:space="preserve"><value>Erstellt</value></data>
|
||||
<data name="Delete" xml:space="preserve"><value>Löschen</value></data>
|
||||
<data name="Document" xml:space="preserve"><value>Dokument</value></data>
|
||||
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Dokument konnte nicht geladen werden</value></data>
|
||||
<data name="DocumentLoading" xml:space="preserve"><value>Dokument wird geladen...</value></data>
|
||||
<data name="EmailAddress" xml:space="preserve"><value>E-Mail-Adresse</value></data>
|
||||
<data name="Envelope" xml:space="preserve"><value>Umschlag</value></data>
|
||||
<data name="Error" xml:space="preserve"><value>Fehler</value></data>
|
||||
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Fehler beim Laden des Dokuments: {0}</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Entwicklungsmodus</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Dies kann dazu führen, dass vertrauliche Informationen aus Ausnahmen Endbenutzern angezeigt werden. Für lokales Debugging setzen Sie ASPNETCORE_ENVIRONMENT auf Development und starten die App neu.</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Im Entwicklungsmodus werden detailliertere Fehlerinformationen angezeigt.</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Der Entwicklungsmodus sollte in bereitgestellten Anwendungen nicht aktiviert sein.</value></data>
|
||||
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten.</value></data>
|
||||
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Anfrage-ID</value></data>
|
||||
<data name="ErrorWithReason" xml:space="preserve"><value>Fehler: {0}</value></data>
|
||||
<data name="From" xml:space="preserve"><value>Von</value></data>
|
||||
<data name="FullName" xml:space="preserve"><value>Vor- und Nachname</value></data>
|
||||
<data name="History" xml:space="preserve"><value>Historie</value></data>
|
||||
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digitale Unterschrift</value></data>
|
||||
<data name="Index_PdfExport" xml:space="preserve"><value>PDF-Export</value></data>
|
||||
<data name="Index_SecureAccess" xml:space="preserve"><value>Sicherer Zugang</value></data>
|
||||
<data name="Index_Subtitle" xml:space="preserve"><value>Willkommen im eSign-Portal</value></data>
|
||||
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Zur Umschlag-Übersicht</value></data>
|
||||
<data name="Languages" xml:space="preserve"><value>Sprachen</value></data>
|
||||
<data name="Loading" xml:space="preserve"><value>Lädt...</value></data>
|
||||
<data name="Login" xml:space="preserve"><value>Anmelden</value></data>
|
||||
<data name="LoginInProgress" xml:space="preserve"><value>Anmelden ...</value></data>
|
||||
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Nicht authentifiziert. Bitte melden Sie sich erneut an.</value></data>
|
||||
<data name="Only" xml:space="preserve"><value>Nur</value></data>
|
||||
<data name="OpenDocument" xml:space="preserve"><value>Dokument öffnen</value></data>
|
||||
<data name="Optional" xml:space="preserve"><value>optional</value></data>
|
||||
<data name="Pending" xml:space="preserve"><value>Ausstehend</value></data>
|
||||
<data name="Preview" xml:space="preserve"><value>Vorschau</value></data>
|
||||
<data name="PrivacyPolicy" xml:space="preserve"><value>Datenschutz</value></data>
|
||||
<data name="Receiver" xml:space="preserve"><value>Empfänger</value></data>
|
||||
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Zugangscode eingeben</value></data>
|
||||
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.</value></data>
|
||||
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.</value></data>
|
||||
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</value></data>
|
||||
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Ungültiger Zugangscode.</value></data>
|
||||
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</value></data>
|
||||
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Dokument nicht gefunden.</value></data>
|
||||
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang mit Zugangscode</value></data>
|
||||
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.</value></data>
|
||||
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Möchten Sie das Dokument verbindlich unterschreiben?</value></data>
|
||||
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope-Schlüssel fehlt.</value></data>
|
||||
<data name="Refresh" xml:space="preserve"><value>Aktualisieren</value></data>
|
||||
<data name="RequiredField" xml:space="preserve"><value>Pflichtfeld</value></data>
|
||||
<data name="Selected" xml:space="preserve"><value>ausgewählte</value></data>
|
||||
<data name="Send" xml:space="preserve"><value>Senden</value></data>
|
||||
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Aktive Umschläge</value></data>
|
||||
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Abgeschlossene Umschläge</value></data>
|
||||
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Neuer Umschlag</value></data>
|
||||
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Neuen Umschlag erstellen</value></data>
|
||||
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Umschlag löschen</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Begründung eingeben...</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Bitte geben Sie einen Löschgrund an.</value></data>
|
||||
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag löschen</value></data>
|
||||
<data name="Sender_Edit" xml:space="preserve"><value>Bearbeiten</value></data>
|
||||
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag bearbeiten</value></data>
|
||||
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Aktuelle Ansicht exportieren</value></data>
|
||||
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Exportformat auswählen</value></data>
|
||||
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export der aktuellen Tabellenansicht</value></data>
|
||||
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Fehler beim Laden der Umschläge</value></data>
|
||||
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Umschläge werden geladen...</value></data>
|
||||
<data name="Sender_NoHistory" xml:space="preserve"><value>Keine Historie vorhanden</value></data>
|
||||
<data name="Sender_NoReceivers" xml:space="preserve"><value>Keine Empfänger</value></data>
|
||||
<data name="Sender_Preview_Title" xml:space="preserve"><value>Ausgewählten Umschlag als PDF anzeigen</value></data>
|
||||
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf.</value></data>
|
||||
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Signatur hinzufügen</value></data>
|
||||
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Klicken Sie auf die Stelle im Dokument für</value></data>
|
||||
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.</value></data>
|
||||
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>E-Mail-Vorschläge werden geladen...</value></data>
|
||||
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Fehler beim Erstellen des Umschlags</value></data>
|
||||
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Fehler beim Laden des Umschlags #{0}: {1}</value></data>
|
||||
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Fehler beim Laden der Datei: {0}</value></data>
|
||||
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Bitte unterzeichnen Sie das beigefügte Dokument.</value></data>
|
||||
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Kein Dokument geladen</value></data>
|
||||
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Keine Seiten zur Anzeige verfügbar.</value></data>
|
||||
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Noch keine Empfänger hinzugefügt.</value></data>
|
||||
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Seite</value></data>
|
||||
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Signaturfeld platzieren</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Alle Signaturfelder entfernen</value></data>
|
||||
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Bitte wählen Sie eine PDF-Datei aus.</value></data>
|
||||
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Der bestehende Entwurf wurde versendet.</value></data>
|
||||
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Umschlag gesendet</value></data>
|
||||
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Umschlag wurde erfolgreich gesendet.</value></data>
|
||||
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signaturfeld</value></data>
|
||||
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>z.B. Arbeitsvertrag 2025</value></data>
|
||||
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Zur Übersicht</value></data>
|
||||
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Diese E-Mail-Adresse wurde bereits hinzugefügt.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Bitte geben Sie eine gültige E-Mail-Adresse ein.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Bitte geben Sie eine E-Mail-Adresse ein.</value></data>
|
||||
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Bitte geben Sie einen Vor- und Nachnamen ein.</value></data>
|
||||
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Bitte fügen Sie mindestens einen Empfänger hinzu.</value></data>
|
||||
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Bitte platzieren Sie mindestens ein Signaturfeld.</value></data>
|
||||
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Bitte geben Sie einen Titel ein.</value></data>
|
||||
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Bitte laden Sie zuerst ein PDF-Dokument hoch.</value></data>
|
||||
<data name="SenderLogin_Description" xml:space="preserve"><value>Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.</value></data>
|
||||
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Administrator.</value></data>
|
||||
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</value></data>
|
||||
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Ungültige Anmeldedaten.</value></data>
|
||||
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Passwort eingeben</value></data>
|
||||
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang zum Sender-Dashboard</value></data>
|
||||
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender Anmeldung</value></data>
|
||||
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Benutzername eingeben</value></data>
|
||||
<data name="ServerError" xml:space="preserve"><value>Serverfehler.</value></data>
|
||||
<data name="Signed" xml:space="preserve"><value>Unterschrieben</value></data>
|
||||
<data name="SignedDocument" xml:space="preserve"><value>Signiertes Dokument</value></data>
|
||||
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</value></data>
|
||||
<data name="UploadPdf" xml:space="preserve"><value>PDF hochladen</value></data>
|
||||
<data name="UserName" xml:space="preserve"><value>Benutzername</value></data>
|
||||
<data name="VerifyInProgress" xml:space="preserve"><value>Überprüfen ...</value></data>
|
||||
</root>
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
<data name="Back" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
</data>
|
||||
<data name="Next" xml:space="preserve">
|
||||
<value>Next</value>
|
||||
</data>
|
||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||
<value>Click the</value>
|
||||
</data>
|
||||
@@ -564,6 +567,12 @@
|
||||
<data name="AddReceiver" xml:space="preserve">
|
||||
<value>Add Receiver</value>
|
||||
</data>
|
||||
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
|
||||
<value>Recently used recipients</value>
|
||||
</data>
|
||||
<data name="Skip" xml:space="preserve">
|
||||
<value>Skip</value>
|
||||
</data>
|
||||
<data name="DeleteReceiver" xml:space="preserve">
|
||||
<value>Delete Receiver</value>
|
||||
</data>
|
||||
@@ -651,4 +660,125 @@
|
||||
<data name="Status" xml:space="preserve">
|
||||
<value>Status</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="ChangedAt" xml:space="preserve"><value>Changed at</value></data>
|
||||
<data name="Close" xml:space="preserve"><value>Close</value></data>
|
||||
<data name="CloseLanguageMenu" xml:space="preserve"><value>Close language menu</value></data>
|
||||
<data name="ConfirmSignature" xml:space="preserve"><value>Confirm signature</value></data>
|
||||
<data name="Created" xml:space="preserve"><value>Created</value></data>
|
||||
<data name="Delete" xml:space="preserve"><value>Delete</value></data>
|
||||
<data name="Document" xml:space="preserve"><value>Document</value></data>
|
||||
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Document could not be loaded</value></data>
|
||||
<data name="DocumentLoading" xml:space="preserve"><value>Document is loading...</value></data>
|
||||
<data name="EmailAddress" xml:space="preserve"><value>Email address</value></data>
|
||||
<data name="Envelope" xml:space="preserve"><value>Envelope</value></data>
|
||||
<data name="Error" xml:space="preserve"><value>Error</value></data>
|
||||
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Error loading document: {0}</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Development Mode</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>This can result in sensitive exception information being shown to end users. For local debugging, set ASPNETCORE_ENVIRONMENT to Development and restart the app.</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Switching to Development will display more detailed error information.</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>The Development environment should not be enabled in deployed applications.</value></data>
|
||||
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>An error occurred while processing your request.</value></data>
|
||||
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Request ID</value></data>
|
||||
<data name="ErrorWithReason" xml:space="preserve"><value>Error: {0}</value></data>
|
||||
<data name="From" xml:space="preserve"><value>From</value></data>
|
||||
<data name="FullName" xml:space="preserve"><value>First and last name</value></data>
|
||||
<data name="History" xml:space="preserve"><value>History</value></data>
|
||||
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digital signature</value></data>
|
||||
<data name="Index_PdfExport" xml:space="preserve"><value>PDF export</value></data>
|
||||
<data name="Index_SecureAccess" xml:space="preserve"><value>Secure access</value></data>
|
||||
<data name="Index_Subtitle" xml:space="preserve"><value>Welcome to the eSign portal</value></data>
|
||||
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Go to envelope overview</value></data>
|
||||
<data name="Languages" xml:space="preserve"><value>Languages</value></data>
|
||||
<data name="Loading" xml:space="preserve"><value>Loading...</value></data>
|
||||
<data name="Login" xml:space="preserve"><value>Sign in</value></data>
|
||||
<data name="LoginInProgress" xml:space="preserve"><value>Signing in ...</value></data>
|
||||
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Not authenticated. Please sign in again.</value></data>
|
||||
<data name="Only" xml:space="preserve"><value>Only</value></data>
|
||||
<data name="OpenDocument" xml:space="preserve"><value>Open document</value></data>
|
||||
<data name="Optional" xml:space="preserve"><value>optional</value></data>
|
||||
<data name="Pending" xml:space="preserve"><value>Pending</value></data>
|
||||
<data name="Preview" xml:space="preserve"><value>Preview</value></data>
|
||||
<data name="PrivacyPolicy" xml:space="preserve"><value>Privacy policy</value></data>
|
||||
<data name="Receiver" xml:space="preserve"><value>Receiver</value></data>
|
||||
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Enter access code</value></data>
|
||||
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Please enter the access code you received by email to securely open the document.</value></data>
|
||||
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the document sender.</value></data>
|
||||
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>The entered code is incorrect. Please try again.</value></data>
|
||||
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Invalid access code.</value></data>
|
||||
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>The provided access code could not be matched to a document. Please verify the link in your email.</value></data>
|
||||
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document not found.</value></data>
|
||||
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Secure access with access code</value></data>
|
||||
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>This action cannot be undone. By confirming, you declare that you have electronically signed the document shown above. The signed document will then be delivered to all involved parties.</value></data>
|
||||
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Do you want to sign this document as binding?</value></data>
|
||||
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope key is missing.</value></data>
|
||||
<data name="Refresh" xml:space="preserve"><value>Refresh</value></data>
|
||||
<data name="RequiredField" xml:space="preserve"><value>Required field</value></data>
|
||||
<data name="Selected" xml:space="preserve"><value>selected</value></data>
|
||||
<data name="Send" xml:space="preserve"><value>Send</value></data>
|
||||
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Active envelopes</value></data>
|
||||
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Completed envelopes</value></data>
|
||||
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>New envelope</value></data>
|
||||
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Create new envelope</value></data>
|
||||
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Delete envelope</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Enter reason...</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Please provide a reason why the envelope should be deleted/withdrawn.</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Please provide a deletion reason.</value></data>
|
||||
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Delete selected envelope</value></data>
|
||||
<data name="Sender_Edit" xml:space="preserve"><value>Edit</value></data>
|
||||
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Edit selected envelope</value></data>
|
||||
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Export current view</value></data>
|
||||
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Select export format</value></data>
|
||||
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export current table view</value></data>
|
||||
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Error loading envelopes</value></data>
|
||||
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Loading envelopes...</value></data>
|
||||
<data name="Sender_NoHistory" xml:space="preserve"><value>No history available</value></data>
|
||||
<data name="Sender_NoReceivers" xml:space="preserve"><value>No receivers</value></data>
|
||||
<data name="Sender_Preview_Title" xml:space="preserve"><value>Show selected envelope as PDF</value></data>
|
||||
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>The document for envelope #{0} was not found or you do not have access.</value></data>
|
||||
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Add signature</value></data>
|
||||
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Click on the location in the document for</value></data>
|
||||
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Previously used email addresses are suggested while typing.</value></data>
|
||||
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Loading email suggestions...</value></data>
|
||||
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Error creating envelope</value></data>
|
||||
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Error loading envelope #{0}: {1}</value></data>
|
||||
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Error loading file: {0}</value></data>
|
||||
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Please sign the attached document.</value></data>
|
||||
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>No document loaded</value></data>
|
||||
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>No pages to display.</value></data>
|
||||
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>No receivers added yet.</value></data>
|
||||
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Place signature field</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Remove all signature fields</value></data>
|
||||
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Please select a PDF file.</value></data>
|
||||
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>The existing draft has been sent.</value></data>
|
||||
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Envelope sent</value></data>
|
||||
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Envelope was sent successfully.</value></data>
|
||||
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signature field</value></data>
|
||||
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>e.g. Employment contract 2025</value></data>
|
||||
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>To overview</value></data>
|
||||
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Upload a PDF file to place signature fields.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>This email address has already been added.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Please enter a valid email address.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Please enter an email address.</value></data>
|
||||
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Please enter first and last name.</value></data>
|
||||
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Please add at least one receiver.</value></data>
|
||||
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Please place at least one signature field.</value></data>
|
||||
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Please enter a title.</value></data>
|
||||
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Please upload a PDF document first.</value></data>
|
||||
<data name="SenderLogin_Description" xml:space="preserve"><value>Please sign in with your credentials to access the sender dashboard.</value></data>
|
||||
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the administrator.</value></data>
|
||||
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Username or password is incorrect. Please try again.</value></data>
|
||||
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Invalid credentials.</value></data>
|
||||
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Enter password</value></data>
|
||||
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Secure access to sender dashboard</value></data>
|
||||
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender login</value></data>
|
||||
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Enter username</value></data>
|
||||
<data name="ServerError" xml:space="preserve"><value>Server error.</value></data>
|
||||
<data name="Signed" xml:space="preserve"><value>Signed</value></data>
|
||||
<data name="SignedDocument" xml:space="preserve"><value>Signed document</value></data>
|
||||
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>An unexpected error occurred. Please try again later.</value></data>
|
||||
<data name="UploadPdf" xml:space="preserve"><value>Upload PDF</value></data>
|
||||
<data name="UserName" xml:space="preserve"><value>Username</value></data>
|
||||
<data name="VerifyInProgress" xml:space="preserve"><value>Verifying ...</value></data>
|
||||
</root>
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
<data name="Back" xml:space="preserve">
|
||||
<value>Retour</value>
|
||||
</data>
|
||||
<data name="Next" xml:space="preserve">
|
||||
<value>Suivant</value>
|
||||
</data>
|
||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||
<value>Cliquez sur</value>
|
||||
</data>
|
||||
@@ -564,6 +567,12 @@
|
||||
<data name="AddReceiver" xml:space="preserve">
|
||||
<value>Ajouter un destinataire</value>
|
||||
</data>
|
||||
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
|
||||
<value>Destinataires recents</value>
|
||||
</data>
|
||||
<data name="Skip" xml:space="preserve">
|
||||
<value>Passer</value>
|
||||
</data>
|
||||
<data name="DeleteReceiver" xml:space="preserve">
|
||||
<value>Supprimer le destinataire</value>
|
||||
</data>
|
||||
@@ -651,4 +660,125 @@
|
||||
<data name="Status" xml:space="preserve">
|
||||
<value>Statut</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="ChangedAt" xml:space="preserve"><value>Modifié le</value></data>
|
||||
<data name="Close" xml:space="preserve"><value>Fermer</value></data>
|
||||
<data name="CloseLanguageMenu" xml:space="preserve"><value>Fermer le menu des langues</value></data>
|
||||
<data name="ConfirmSignature" xml:space="preserve"><value>Confirmer la signature</value></data>
|
||||
<data name="Created" xml:space="preserve"><value>Créé</value></data>
|
||||
<data name="Delete" xml:space="preserve"><value>Supprimer</value></data>
|
||||
<data name="Document" xml:space="preserve"><value>Document</value></data>
|
||||
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Le document n'a pas pu être chargé</value></data>
|
||||
<data name="DocumentLoading" xml:space="preserve"><value>Chargement du document...</value></data>
|
||||
<data name="EmailAddress" xml:space="preserve"><value>Adresse e-mail</value></data>
|
||||
<data name="Envelope" xml:space="preserve"><value>Enveloppe</value></data>
|
||||
<data name="Error" xml:space="preserve"><value>Erreur</value></data>
|
||||
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Erreur lors du chargement du document : {0}</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Mode développement</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Cela peut afficher des informations sensibles d'exception aux utilisateurs finaux. Pour le débogage local, définissez ASPNETCORE_ENVIRONMENT sur Development et redémarrez l'application.</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Le mode développement affiche des informations d'erreur plus détaillées.</value></data>
|
||||
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Le mode développement ne doit pas être activé dans les applications déployées.</value></data>
|
||||
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Une erreur s'est produite lors du traitement de votre demande.</value></data>
|
||||
<data name="ErrorPage_RequestId" xml:space="preserve"><value>ID de requête</value></data>
|
||||
<data name="ErrorWithReason" xml:space="preserve"><value>Erreur : {0}</value></data>
|
||||
<data name="From" xml:space="preserve"><value>De</value></data>
|
||||
<data name="FullName" xml:space="preserve"><value>Nom et prénom</value></data>
|
||||
<data name="History" xml:space="preserve"><value>Historique</value></data>
|
||||
<data name="Index_DigitalSignature" xml:space="preserve"><value>Signature numérique</value></data>
|
||||
<data name="Index_PdfExport" xml:space="preserve"><value>Export PDF</value></data>
|
||||
<data name="Index_SecureAccess" xml:space="preserve"><value>Accès sécurisé</value></data>
|
||||
<data name="Index_Subtitle" xml:space="preserve"><value>Bienvenue sur le portail eSign</value></data>
|
||||
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Aller à l'aperçu des enveloppes</value></data>
|
||||
<data name="Languages" xml:space="preserve"><value>Langues</value></data>
|
||||
<data name="Loading" xml:space="preserve"><value>Chargement...</value></data>
|
||||
<data name="Login" xml:space="preserve"><value>Connexion</value></data>
|
||||
<data name="LoginInProgress" xml:space="preserve"><value>Connexion ...</value></data>
|
||||
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Non authentifié. Veuillez vous reconnecter.</value></data>
|
||||
<data name="Only" xml:space="preserve"><value>Uniquement</value></data>
|
||||
<data name="OpenDocument" xml:space="preserve"><value>Ouvrir le document</value></data>
|
||||
<data name="Optional" xml:space="preserve"><value>optionnel</value></data>
|
||||
<data name="Pending" xml:space="preserve"><value>En attente</value></data>
|
||||
<data name="Preview" xml:space="preserve"><value>Aperçu</value></data>
|
||||
<data name="PrivacyPolicy" xml:space="preserve"><value>Politique de confidentialité</value></data>
|
||||
<data name="Receiver" xml:space="preserve"><value>Destinataire</value></data>
|
||||
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Saisir le code d'accès</value></data>
|
||||
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Veuillez saisir le code d'accès reçu par e-mail pour ouvrir le document en toute sécurité.</value></data>
|
||||
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'expéditeur du document.</value></data>
|
||||
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Le code saisi est incorrect. Veuillez réessayer.</value></data>
|
||||
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Code d'accès invalide.</value></data>
|
||||
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Le code d'accès indiqué ne correspond à aucun document. Veuillez vérifier le lien dans votre e-mail.</value></data>
|
||||
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document introuvable.</value></data>
|
||||
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé avec code d'accès</value></data>
|
||||
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Cette action est irréversible. En confirmant, vous déclarez avoir signé électroniquement le document affiché ci-dessus. Le document signé sera ensuite transmis à toutes les parties concernées.</value></data>
|
||||
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Voulez-vous signer ce document de manière contraignante ?</value></data>
|
||||
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>La clé d'enveloppe est manquante.</value></data>
|
||||
<data name="Refresh" xml:space="preserve"><value>Actualiser</value></data>
|
||||
<data name="RequiredField" xml:space="preserve"><value>Champ obligatoire</value></data>
|
||||
<data name="Selected" xml:space="preserve"><value>sélectionnées</value></data>
|
||||
<data name="Send" xml:space="preserve"><value>Envoyer</value></data>
|
||||
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Enveloppes actives</value></data>
|
||||
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Enveloppes terminées</value></data>
|
||||
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Nouvelle enveloppe</value></data>
|
||||
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Créer une nouvelle enveloppe</value></data>
|
||||
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Supprimer l'enveloppe</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Saisir la raison...</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Veuillez indiquer la raison pour laquelle l'enveloppe doit être supprimée/retirée.</value></data>
|
||||
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Veuillez indiquer un motif de suppression.</value></data>
|
||||
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Supprimer l'enveloppe sélectionnée</value></data>
|
||||
<data name="Sender_Edit" xml:space="preserve"><value>Modifier</value></data>
|
||||
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Modifier l'enveloppe sélectionnée</value></data>
|
||||
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Exporter la vue actuelle</value></data>
|
||||
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Sélectionner le format d'export</value></data>
|
||||
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Exporter la vue actuelle du tableau</value></data>
|
||||
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Erreur lors du chargement des enveloppes</value></data>
|
||||
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Chargement des enveloppes...</value></data>
|
||||
<data name="Sender_NoHistory" xml:space="preserve"><value>Aucun historique disponible</value></data>
|
||||
<data name="Sender_NoReceivers" xml:space="preserve"><value>Aucun destinataire</value></data>
|
||||
<data name="Sender_Preview_Title" xml:space="preserve"><value>Afficher l'enveloppe sélectionnée en PDF</value></data>
|
||||
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Le document de l'enveloppe #{0} est introuvable ou vous n'avez pas l'autorisation d'y accéder.</value></data>
|
||||
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Ajouter une signature</value></data>
|
||||
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Cliquez sur l'emplacement dans le document pour</value></data>
|
||||
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Les adresses e-mail déjà utilisées sont proposées pendant la saisie.</value></data>
|
||||
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Chargement des suggestions d'e-mail...</value></data>
|
||||
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Erreur lors de la création de l'enveloppe</value></data>
|
||||
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Erreur lors du chargement de l'enveloppe #{0} : {1}</value></data>
|
||||
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Erreur lors du chargement du fichier : {0}</value></data>
|
||||
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Veuillez signer le document ci-joint.</value></data>
|
||||
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Aucun document chargé</value></data>
|
||||
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Aucune page à afficher.</value></data>
|
||||
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Aucun destinataire ajouté pour le moment.</value></data>
|
||||
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Placer un champ de signature</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Supprimer tous les champs de signature</value></data>
|
||||
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Veuillez sélectionner un fichier PDF.</value></data>
|
||||
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Le brouillon existant a été envoyé.</value></data>
|
||||
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Enveloppe envoyée</value></data>
|
||||
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>L'enveloppe a été envoyée avec succès.</value></data>
|
||||
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Champ de signature</value></data>
|
||||
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>p. ex. Contrat de travail 2025</value></data>
|
||||
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Vers l'aperçu</value></data>
|
||||
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Téléversez un fichier PDF pour placer des champs de signature.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Cette adresse e-mail a déjà été ajoutée.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Veuillez saisir une adresse e-mail valide.</value></data>
|
||||
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Veuillez saisir une adresse e-mail.</value></data>
|
||||
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Veuillez saisir le nom et le prénom.</value></data>
|
||||
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Veuillez ajouter au moins un destinataire.</value></data>
|
||||
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Veuillez placer au moins un champ de signature.</value></data>
|
||||
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Veuillez saisir un titre.</value></data>
|
||||
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Veuillez d'abord téléverser un document PDF.</value></data>
|
||||
<data name="SenderLogin_Description" xml:space="preserve"><value>Veuillez vous connecter avec vos identifiants pour accéder au tableau de bord expéditeur.</value></data>
|
||||
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'administrateur.</value></data>
|
||||
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Le nom d'utilisateur ou le mot de passe est incorrect. Veuillez réessayer.</value></data>
|
||||
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Identifiants invalides.</value></data>
|
||||
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Saisir le mot de passe</value></data>
|
||||
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé au tableau de bord expéditeur</value></data>
|
||||
<data name="SenderLogin_Title" xml:space="preserve"><value>Connexion expéditeur</value></data>
|
||||
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Saisir le nom d'utilisateur</value></data>
|
||||
<data name="ServerError" xml:space="preserve"><value>Erreur serveur.</value></data>
|
||||
<data name="Signed" xml:space="preserve"><value>Signé</value></data>
|
||||
<data name="SignedDocument" xml:space="preserve"><value>Document signé</value></data>
|
||||
<data name="System" xml:space="preserve"><value>Système</value></data>
|
||||
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Une erreur inattendue s'est produite. Veuillez réessayer plus tard.</value></data>
|
||||
<data name="UploadPdf" xml:space="preserve"><value>Téléverser un PDF</value></data>
|
||||
<data name="UserName" xml:space="preserve"><value>Nom d'utilisateur</value></data>
|
||||
<data name="VerifyInProgress" xml:space="preserve"><value>Vérification ...</value></data>
|
||||
</root>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
{
|
||||
public enum CertificationType
|
||||
{
|
||||
AdvancedElectronicSignature = 1
|
||||
// ElectronicSignature = 1
|
||||
// QualifiedSignature = 2
|
||||
AdvancedElectronicSignature = 1,
|
||||
ElectronicSignature = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace EnvelopeGenerator.Domain.Constants
|
||||
{
|
||||
public static bool IsActive(this EnvelopeStatus status)
|
||||
{
|
||||
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
|
||||
return status >= EnvelopeStatus.EnvelopeCreated && status <= EnvelopeStatus.EnvelopePartlySigned;
|
||||
}
|
||||
|
||||
public static bool IsCompleted(this EnvelopeStatus status)
|
||||
|
||||
@@ -11,7 +11,7 @@ using System.Collections.Generic;
|
||||
namespace EnvelopeGenerator.Domain.Entities
|
||||
{
|
||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
||||
public class DocReceiverElement : IDocReceiverElement, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
||||
public class DocReceiverElement : IDocReceiverElement, ISignatureInkPayload, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
||||
{
|
||||
public DocReceiverElement()
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
SendReminderEmails = false;
|
||||
FirstReminderDays = 0;
|
||||
ReminderIntervalDays = 0;
|
||||
CertificationType = (int)Constants.CertificationType.AdvancedElectronicSignature;
|
||||
CertificationType = Constants.CertificationType.AdvancedElectronicSignature;
|
||||
UseAccessCode = false;
|
||||
Documents = Enumerable.Empty<Document>().ToList();
|
||||
Histories = Enumerable.Empty<History>().ToList();
|
||||
@@ -83,7 +83,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
public string Comment { get; set; }
|
||||
|
||||
[Column("CONTRACT_TYPE")]
|
||||
public int? ContractType { get; set; }
|
||||
public ContractType? ContractType { get; set; }
|
||||
|
||||
#if NETFRAMEWORK
|
||||
[NotMapped]
|
||||
@@ -111,7 +111,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
public bool ReadOnly => this.IsReadAndConfirm();
|
||||
|
||||
[Column("CERTIFICATION_TYPE")]
|
||||
public int? CertificationType { get; set; }
|
||||
public CertificationType? CertificationType { get; set; }
|
||||
|
||||
[Column("USE_ACCESS_CODE")]
|
||||
public bool? UseAccessCode { get; set; }
|
||||
@@ -129,7 +129,11 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
public int? ExpiresWarningWhenDays { get; set; }
|
||||
|
||||
[ForeignKey("UserId")]
|
||||
public User User { get; set; }
|
||||
public User
|
||||
#if nullable
|
||||
?
|
||||
#endif
|
||||
User { get; set; }
|
||||
|
||||
[Column("TFA_ENABLED")]
|
||||
public bool? TfaEnabled { get; set; }
|
||||
@@ -137,7 +141,6 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
= false;
|
||||
#endif
|
||||
|
||||
[NotMapped]
|
||||
[Column("DOC_RESULT")]
|
||||
public byte[]
|
||||
#if nullable
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
@@ -22,41 +24,41 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
public string Language { get; set; }
|
||||
|
||||
[Column("EXPIRES_DAYS")]
|
||||
public int ExpiresDays { get; set; }
|
||||
public int? ExpiresDays { get; set; }
|
||||
|
||||
[Column("CERTIFICATION_TYPE")]
|
||||
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature;
|
||||
public Constants.CertificationType? CertificationType { get; set; }
|
||||
|
||||
[Column("USE_ACCESS_CODE")]
|
||||
public bool UseAccessCode { get; set; }
|
||||
public bool? UseAccessCode { get; set; }
|
||||
|
||||
[Column("FINAL_EMAIL_TO_CREATOR")]
|
||||
public Constants.FinalEmailType FinalEmailToCreator { get; set; }
|
||||
public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
|
||||
|
||||
[Column("FINAL_EMAIL_TO_RECEIVERS")]
|
||||
public Constants.FinalEmailType FinalEmailToReceivers { get; set; }
|
||||
public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("ADDED_WHEN", TypeName = "datetime")]
|
||||
public DateTime AddedWhen { get; set; }
|
||||
|
||||
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
||||
public DateTime ChangedWhen { get; set; }
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
|
||||
[Column("EXPIRES_WARNING_DAYS")]
|
||||
public int ExpiresWarningDays { get; set; }
|
||||
public int? ExpiresWarningDays { get; set; }
|
||||
|
||||
[Column("SEND_REMINDER_EMAILS")]
|
||||
public bool SendReminderEmails { get; set; } = false;
|
||||
public bool? SendReminderEmails { get; set; }
|
||||
|
||||
[Column("FIRST_REMINDER_DAYS")]
|
||||
public int FirstReminderDays { get; set; } = 0;
|
||||
public int? FirstReminderDays { get; set; }
|
||||
|
||||
[Column("REMINDER_INTERVAL_DAYS")]
|
||||
public int ReminderIntervalDays { get; set; } = 0;
|
||||
public int? ReminderIntervalDays { get; set; }
|
||||
|
||||
[Column("CONTRACT_TYPE")]
|
||||
public int ContractType { get; set; }
|
||||
public ContractType? ContractType { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public bool TFA_Enabled { get; set; } = false;
|
||||
@@ -70,14 +72,14 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
[NotMapped]
|
||||
public int ExpiresWhenDays
|
||||
{
|
||||
get => ExpiresDays;
|
||||
get => ExpiresDays ?? 0;
|
||||
set => ExpiresDays = value;
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public int ExpiresWarningWhenDays
|
||||
{
|
||||
get => ExpiresWarningDays;
|
||||
get => ExpiresWarningDays ?? 0;
|
||||
set => ExpiresWarningDays = value;
|
||||
}
|
||||
|
||||
@@ -86,4 +88,4 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
return Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net462;net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
||||
<FileVersion>1.4.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Exceptions
|
||||
{
|
||||
public class AuthorizationException : Exception
|
||||
{
|
||||
public AuthorizationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AuthorizationException()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Exceptions
|
||||
{
|
||||
public class DataIntegrityException : Exception
|
||||
{
|
||||
public DataIntegrityException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public DataIntegrityException() : base()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@
|
||||
public interface IEnvelope
|
||||
{
|
||||
int? EnvelopeTypeId { get; set; }
|
||||
|
||||
byte[]
|
||||
#if nullable
|
||||
?
|
||||
#endif
|
||||
DocResult { get; }
|
||||
}
|
||||
|
||||
public static class EnvelopeExtensions
|
||||
@@ -16,5 +22,10 @@
|
||||
{
|
||||
return envelope.EnvelopeTypeId != 2;
|
||||
}
|
||||
|
||||
public static bool IsBurned(this IEnvelope envelope)
|
||||
{
|
||||
return envelope.DocResult != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
EnvelopeGenerator.Domain/Interfaces/ISignatureInkPayload.cs
Normal file
28
EnvelopeGenerator.Domain/Interfaces/ISignatureInkPayload.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Interfaces
|
||||
{
|
||||
public interface ISignatureInkPayload
|
||||
{
|
||||
#if nullable
|
||||
byte[]? Ink { get; }
|
||||
|
||||
string? FullName { get; }
|
||||
|
||||
string? Position { get; }
|
||||
|
||||
string? Place { get; }
|
||||
#else
|
||||
byte[] Ink { get; }
|
||||
|
||||
string FullName { get; }
|
||||
|
||||
string Position { get; }
|
||||
|
||||
string Place { get; }
|
||||
#endif
|
||||
DateTime? ChangedWhen { get; }
|
||||
}
|
||||
}
|
||||
7
EnvelopeGenerator.Domain/Resources/Model.cs
Normal file
7
EnvelopeGenerator.Domain/Resources/Model.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace EnvelopeGenerator.Domain.Resources
|
||||
{
|
||||
// Marker type for IStringLocalizer lookup of Resources/Model.resx.
|
||||
public class Model
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -276,4 +276,61 @@
|
||||
<data name="YesWithAttachment" xml:space="preserve">
|
||||
<value>Yes, with Attachment</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="ElectronicSignature" xml:space="preserve">
|
||||
<value>Electronic Signature</value>
|
||||
</data>
|
||||
<data name="EnvelopeType" xml:space="preserve">
|
||||
<value>Envelope Type</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Language</value>
|
||||
</data>
|
||||
<data name="CertificationType" xml:space="preserve">
|
||||
<value>Certification</value>
|
||||
</data>
|
||||
<data name="TFAEnabled" xml:space="preserve">
|
||||
<value>2FA enabled</value>
|
||||
</data>
|
||||
<data name="UseAccessCode" xml:space="preserve">
|
||||
<value>Use Access Code</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Final Email to Creator</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Final Email to Receivers</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Send Reminder Emails</value>
|
||||
</data>
|
||||
<data name="FirstReminderDays" xml:space="preserve">
|
||||
<value>First Reminder (Days)</value>
|
||||
</data>
|
||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||
<value>Reminder Interval (Days)</value>
|
||||
</data>
|
||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||
<value>Expires After (Days)</value>
|
||||
</data>
|
||||
<data name="ExpiresWarningWhenDays" xml:space="preserve">
|
||||
<value>Expiry Warning (Days)</value>
|
||||
</data>
|
||||
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
|
||||
<value>Envelope Settings</value>
|
||||
</data>
|
||||
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
|
||||
<value>Advanced</value>
|
||||
</data>
|
||||
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
|
||||
<value>Select Template</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
|
||||
<value>Please enter a reminder interval greater than 0 days.</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
|
||||
<value>Please enter valid days for the first reminder.</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
|
||||
<value>The expiry warning cannot be after the expiry date.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
173
EnvelopeGenerator.Domain/Resources/Model.fr-FR.resx
Normal file
173
EnvelopeGenerator.Domain/Resources/Model.fr-FR.resx
Normal file
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
|
||||
<data name="AccessCodeIncorrect" xml:space="preserve">
|
||||
<value>Code d'acces incorrect</value>
|
||||
</data>
|
||||
<data name="DocumentRejected" xml:space="preserve">
|
||||
<value>Document rejete</value>
|
||||
</data>
|
||||
<data name="DocumentSigned" xml:space="preserve">
|
||||
<value>Document signe</value>
|
||||
</data>
|
||||
<data name="Draft" xml:space="preserve">
|
||||
<value>Brouillon</value>
|
||||
</data>
|
||||
<data name="EnvelopeCompletelySigned" xml:space="preserve">
|
||||
<value>Entierement signe</value>
|
||||
</data>
|
||||
<data name="EnvelopeCreated" xml:space="preserve">
|
||||
<value>Enveloppe creee</value>
|
||||
</data>
|
||||
<data name="EnvelopeDeleted" xml:space="preserve">
|
||||
<value>Enveloppe supprimee</value>
|
||||
</data>
|
||||
<data name="EnvelopePartlySigned" xml:space="preserve">
|
||||
<value>Partiellement signee</value>
|
||||
</data>
|
||||
<data name="EnvelopeQueued" xml:space="preserve">
|
||||
<value>Enveloppe en file d'attente</value>
|
||||
</data>
|
||||
<data name="EnvelopeRejected" xml:space="preserve">
|
||||
<value>Enveloppe rejetee</value>
|
||||
</data>
|
||||
<data name="EnvelopeSaved" xml:space="preserve">
|
||||
<value>Enregistree</value>
|
||||
</data>
|
||||
<data name="EnvelopeSent" xml:space="preserve">
|
||||
<value>Envoyee</value>
|
||||
</data>
|
||||
<data name="EnvelopeWithdrawn" xml:space="preserve">
|
||||
<value>Enveloppe retiree</value>
|
||||
</data>
|
||||
<data name="MessageAccessCodeSent" xml:space="preserve">
|
||||
<value>Code d'acces envoye</value>
|
||||
</data>
|
||||
<data name="MessageCompletionSent" xml:space="preserve">
|
||||
<value>E-mail de cloture envoye</value>
|
||||
</data>
|
||||
<data name="MessageConfirmationSent" xml:space="preserve">
|
||||
<value>Confirmation finale envoyee</value>
|
||||
</data>
|
||||
<data name="MessageDeletionSent" xml:space="preserve">
|
||||
<value>Notification de suppression envoyee</value>
|
||||
</data>
|
||||
<data name="MessageInvitationSent" xml:space="preserve">
|
||||
<value>Invitation envoyee</value>
|
||||
</data>
|
||||
<data name="ElectronicSignature" xml:space="preserve">
|
||||
<value>Signature electronique</value>
|
||||
</data>
|
||||
<data name="EnvelopeType" xml:space="preserve">
|
||||
<value>Type d'enveloppe</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Langue</value>
|
||||
</data>
|
||||
<data name="CertificationType" xml:space="preserve">
|
||||
<value>Certification</value>
|
||||
</data>
|
||||
<data name="TFAEnabled" xml:space="preserve">
|
||||
<value>2FA active</value>
|
||||
</data>
|
||||
<data name="UseAccessCode" xml:space="preserve">
|
||||
<value>Utiliser un code d'acces</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>E-mail final au createur</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>E-mail final aux destinataires</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Envoyer des e-mails de rappel</value>
|
||||
</data>
|
||||
<data name="FirstReminderDays" xml:space="preserve">
|
||||
<value>Premier rappel (jours)</value>
|
||||
</data>
|
||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||
<value>Intervalle de rappel (jours)</value>
|
||||
</data>
|
||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||
<value>Expire apres (jours)</value>
|
||||
</data>
|
||||
<data name="ExpiresWarningWhenDays" xml:space="preserve">
|
||||
<value>Avertissement d'expiration (jours)</value>
|
||||
</data>
|
||||
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
|
||||
<value>Parametres de l'enveloppe</value>
|
||||
</data>
|
||||
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
|
||||
<value>Avance</value>
|
||||
</data>
|
||||
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
|
||||
<value>Selectionner un modele</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
|
||||
<value>Veuillez saisir un intervalle de rappel superieur a 0 jour.</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
|
||||
<value>Veuillez saisir des jours valides pour le premier rappel.</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
|
||||
<value>L'avertissement d'expiration ne peut pas etre apres la date d'expiration.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -279,4 +279,61 @@
|
||||
<data name="YesWithAttachment" xml:space="preserve">
|
||||
<value>Ja, mit Anhang</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="ElectronicSignature" xml:space="preserve">
|
||||
<value>Elektronische Signatur</value>
|
||||
</data>
|
||||
<data name="EnvelopeType" xml:space="preserve">
|
||||
<value>Umschlagtyp</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Sprache</value>
|
||||
</data>
|
||||
<data name="CertificationType" xml:space="preserve">
|
||||
<value>Zertifizierung</value>
|
||||
</data>
|
||||
<data name="TFAEnabled" xml:space="preserve">
|
||||
<value>2FA aktiviert</value>
|
||||
</data>
|
||||
<data name="UseAccessCode" xml:space="preserve">
|
||||
<value>Zugriffscode verwenden</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Finale E-Mail an Ersteller</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Finale E-Mail an Empfänger</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Erinnerungs-E-Mails senden</value>
|
||||
</data>
|
||||
<data name="FirstReminderDays" xml:space="preserve">
|
||||
<value>Erste Erinnerung (Tage)</value>
|
||||
</data>
|
||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||
<value>Erinnerungsintervall (Tage)</value>
|
||||
</data>
|
||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||
<value>Läuft ab nach (Tage)</value>
|
||||
</data>
|
||||
<data name="ExpiresWarningWhenDays" xml:space="preserve">
|
||||
<value>Ablaufwarnung (Tage)</value>
|
||||
</data>
|
||||
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
|
||||
<value>Envelope-Einstellungen</value>
|
||||
</data>
|
||||
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
|
||||
<value>Erweitert</value>
|
||||
</data>
|
||||
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
|
||||
<value>Vorlage auswählen</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
|
||||
<value>Bitte geben Sie ein Erinnerungsintervall größer als 0 Tage ein.</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
|
||||
<value>Bitte geben Sie gültige Tage für die erste Erinnerung ein.</value>
|
||||
</data>
|
||||
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
|
||||
<value>Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -105,10 +105,10 @@ Public Class frmEnvelopeMainData
|
||||
End If
|
||||
End Sub
|
||||
Private Function TranslateCertificationType(pType As CertificationType) As String
|
||||
Return Resources.Model.ResourceManager.GetString(pType.ToString())
|
||||
Return CommonServices.My.Resources.Model.ResourceManager.GetString(pType.ToString())
|
||||
End Function
|
||||
Private Function TranslateFinalEmailType(pType As FinalEmailType) As String
|
||||
Return Resources.Model.ResourceManager.GetString(pType.ToString())
|
||||
Return CommonServices.My.Resources.Model.ResourceManager.GetString(pType.ToString())
|
||||
End Function
|
||||
|
||||
Private Sub btOK_Click(sender As Object, e As EventArgs) Handles btOK.Click
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using DocumentService.Client.Configuration;
|
||||
using DocumentService.Client.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Infrastructure.Crypt.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Crypt.DependencyInjection;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddCryptInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var baseUrl = configuration["DocumentService:BaseUrl"]
|
||||
?? throw new InvalidOperationException("DocumentService:BaseUrl is not configured.");
|
||||
|
||||
services.AddDocumentServiceClients(options => options.BaseUrl = baseUrl);
|
||||
|
||||
services.AddSingleton<IPdfDigitalSignatureService, PdfDigitalSignatureService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentService.Client" Version="1.3.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
384
EnvelopeGenerator.Infrastructure.Crypt/README.md
Normal file
384
EnvelopeGenerator.Infrastructure.Crypt/README.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# EnvelopeGenerator.Infrastructure.Crypt
|
||||
|
||||
Umfassende technische Dokumentation der PKCS#11/SoftHSM-Integration fuer `EnvelopeGenerator`.
|
||||
|
||||
Dieses Dokument konsolidiert und ersetzt die verteilten Runbooks aus:
|
||||
|
||||
- `pkcs11-proxy-production/README.md`
|
||||
- `pkcs11-proxy-production/SSH_TEMP_ACCESS_MUST_DO.md`
|
||||
- aeltere Abschnitte in diesem README
|
||||
|
||||
Zielgruppe: Senior Developer, Platform Engineers, DevOps, Security Owner.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architekturueberblick
|
||||
|
||||
### 1.1 Ziel
|
||||
|
||||
`EnvelopeGenerator.Infrastructure.Crypt` stellt die kryptographische Infrastruktur fuer PDF-Signaturen bereit:
|
||||
|
||||
- Signaturerzeugung mit Schluesseln in SoftHSM/PKCS#11
|
||||
- Zertifikat-Auslese aus Token
|
||||
- Signaturpruefung auf PDF-Ebene
|
||||
- Health/Startup-Selbsttest fuer Betriebsfaehigkeit
|
||||
|
||||
### 1.2 Topologie (aktueller Betriebsmodus)
|
||||
|
||||
Der aktuell implementierte Produktions-nahe Pfad ist:
|
||||
|
||||
1. Linux hostet SoftHSM + `pkcs11-daemon` (Proxy-Server).
|
||||
2. Windows hostet `EnvelopeGenerator.Server`.
|
||||
3. Windows laedt ein Windows-kompatibles Proxy-PKCS#11-Modul (`libpkcs11-proxy.dll`).
|
||||
4. PKCS#11-Operationen werden ueber Socket (`PKCS11_PROXY_SOCKET`) an Linux weitergeleitet.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Ein Windows-Prozess kann **kein Linux `.so`** direkt laden.
|
||||
- Private Keys verbleiben im Token (nicht exportierbar).
|
||||
|
||||
### 1.3 Laufzeitpfade in der Anwendung
|
||||
|
||||
`Pkcs11InteropBootstrap` loest Assets relativ zu `Crypt:Pkcs11:AssetsRootPath` auf:
|
||||
|
||||
- Modul: `pkcs11\windows\proxy\libpkcs11-proxy.dll`
|
||||
- Tool-Fallback: `pkcs11\windows\opensc\tools\pkcs11-tool.exe`
|
||||
|
||||
Wenn `AssetsRootPath` leer ist, wird `AppContext.BaseDirectory` verwendet.
|
||||
|
||||
---
|
||||
|
||||
## 2. Linux-Seite: SoftHSM + Proxy Setup
|
||||
|
||||
### 2.1 Basispakete
|
||||
|
||||
Beispiel (Debian/Ubuntu):
|
||||
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install -y softhsm2 opensc p11-kit libengine-pkcs11-openssl
|
||||
```
|
||||
|
||||
### 2.2 SoftHSM-Modulpfad pruefen
|
||||
|
||||
```bash
|
||||
ldconfig -p | grep softhsm
|
||||
find /usr -name "libsofthsm2.so" 2>/dev/null
|
||||
```
|
||||
|
||||
### 2.3 Token initialisieren
|
||||
|
||||
```bash
|
||||
softhsm2-util --show-slots
|
||||
softhsm2-util --init-token --slot <uninitialized-slot> --label CompanyProdToken --so-pin <so-pin> --pin <user-pin>
|
||||
softhsm2-util --show-slots
|
||||
```
|
||||
|
||||
Hinweis: Nach `--init-token` wird der Slot i. d. R. neu zugewiesen.
|
||||
|
||||
### 2.4 Schluesselpaar erzeugen
|
||||
|
||||
```bash
|
||||
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
|
||||
--slot <slot-id> --login --pin <user-pin> \
|
||||
--keypairgen --key-type rsa:3072 --label company-sign-key-v1 --id 01
|
||||
```
|
||||
|
||||
### 2.5 Zertifikat importieren
|
||||
|
||||
```bash
|
||||
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
|
||||
--slot <slot-id> --login --pin <user-pin> \
|
||||
--write-object ./company-sign-cert-v1.der --type cert --id 01 --label company-sign-cert-v1
|
||||
```
|
||||
|
||||
### 2.6 Objektzustand verifizieren
|
||||
|
||||
```bash
|
||||
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> --list-objects
|
||||
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> --list-objects --type cert
|
||||
```
|
||||
|
||||
### 2.7 Proxy-Daemon endpoint stabilisieren
|
||||
|
||||
Ein wiederkehrendes Betriebsproblem war ein zufaelliger/local-only Listener. Fix:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/pkcs11-proxy.service.d
|
||||
printf "[Service]\nEnvironment=PKCS11_DAEMON_SOCKET=tcp://0.0.0.0:5657\n" | sudo tee /etc/systemd/system/pkcs11-proxy.service.d/override.conf
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart pkcs11-proxy.service
|
||||
```
|
||||
|
||||
Pruefung:
|
||||
|
||||
```bash
|
||||
systemctl show pkcs11-proxy.service -p Environment --no-pager
|
||||
ss -ltnp | grep 5657
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Web-API/Server-Seite: Installation und Asset-Bereitstellung
|
||||
|
||||
### 3.1 Bezugsquellen der Windows-Artefakte
|
||||
|
||||
Fuer den Proxy-Betrieb werden auf Windows benoetigt:
|
||||
|
||||
1. Proxy-PKCS#11-DLL (`libpkcs11-proxy.dll`)
|
||||
2. Abhaengige Runtime-DLLs (z. B. MinGW runtime, je nach Build)
|
||||
3. `pkcs11-tool.exe` + zugehoerige OpenSC-DLLs fuer Fallback
|
||||
|
||||
Beschaffung:
|
||||
|
||||
- Build aus derselben `pkcs11-proxy`-Codebasis/Commit wie Linux-Daemon (Kompatibilitaet sicherstellen)
|
||||
- oder kontrolliert bereitgestellte, intern freigegebene Binaries
|
||||
|
||||
### 3.2 Empfohlene Ablage im Repo
|
||||
|
||||
Bundled Assets liegen unter:
|
||||
|
||||
- `EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/`
|
||||
- `EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/`
|
||||
- `EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/pkcs11/`
|
||||
|
||||
Diese werden beim Build in den Output kopiert (siehe `.csproj`-Copy-Regeln).
|
||||
|
||||
### 3.3 Deployment-Muster
|
||||
|
||||
Es gibt zwei gueltige Wege:
|
||||
|
||||
1. **Bundled (Dev/QA, schnelle Reproduzierbarkeit)**
|
||||
- Assets werden mit dem Build ausgeliefert.
|
||||
- `AssetsRootPath = null` -> Aufloesung via `AppContext.BaseDirectory`.
|
||||
|
||||
2. **Zentraler Asset-Root (Prod, betriebliche Kontrolle)**
|
||||
- Assets in zentralem Pfad (z. B. `E:\DataFiles\Digital Data\EnvelopeGenerator`).
|
||||
- `AssetsRootPath` in `appsettings.json` auf diesen Root setzen.
|
||||
|
||||
### 3.4 Anwendungskonfiguration (`Crypt:Pkcs11`)
|
||||
|
||||
Beispiel:
|
||||
|
||||
```json
|
||||
{
|
||||
"Crypt": {
|
||||
"Pkcs11": {
|
||||
"AssetsRootPath": "E:\\DataFiles\\Digital Data\\EnvelopeGenerator",
|
||||
"SlotId": 2021549410,
|
||||
"TokenLabel": "CompanyProdToken",
|
||||
"ProxySocket": "tcp://172.24.12.64:5657",
|
||||
"UserPin": "<secret>",
|
||||
"PrivateKeyLabel": "company-sign-key-v1",
|
||||
"CertificateLabel": "company-sign-cert-v1",
|
||||
"ApplicationName": "signFLOW",
|
||||
"DefaultReason": null,
|
||||
"UseUtcSigningTime": true,
|
||||
"DefaultCertificationLevel": "NoCertification"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Sicherheitsregel:
|
||||
|
||||
- `UserPin` niemals in versionierte Dateien einchecken.
|
||||
- Secrets nur in Secret Store / nicht versionierte `appsettings.Secrets.json` / Vault.
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementierte Services, Modelle, DTOs
|
||||
|
||||
### 4.1 DI und Startverhalten
|
||||
|
||||
- `AddCryptInfrastructure(...)` in `DependencyInjection/ServiceCollectionExtensions.cs`
|
||||
- bindet `Pkcs11Options`
|
||||
- registriert `IPdfDigitalSignatureService`
|
||||
- registriert Startup-Self-Test Hosted Service
|
||||
|
||||
### 4.2 Konfigurationsmodell
|
||||
|
||||
- `Pkcs11Options` (`EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs`)
|
||||
- `AssetsRootPath`, `SlotId`, `TokenLabel`, `ProxySocket`
|
||||
- `UserPin`, `PrivateKeyLabel`, `CertificateLabel`
|
||||
- `ApplicationName`, `DefaultReason`, `UseUtcSigningTime`, `DefaultCertificationLevel`
|
||||
- `ResolvedLibraryPath`, `ResolvedToolPath` (Runtime-resolved)
|
||||
|
||||
### 4.3 Kernservices
|
||||
|
||||
1. `PdfDigitalSignatureService`
|
||||
- Signiert PDF via DevExpress `PdfDeferredSignatureBuilder`
|
||||
- Nutzt `Pkcs11ExternalSigner` fuer externe Signatur
|
||||
- Erzwingt `ApplicationName` aus Config (nicht client-overridable)
|
||||
- Erweitert `Reason` um strukturiertes Metaformat:
|
||||
- `SFMETA:v1;doc=<uuid>;sha256=<hash>`
|
||||
- Parse beim Verify und Mapping in DTO
|
||||
|
||||
2. `Pkcs11InteropBootstrap`
|
||||
- Loest Modul-/Toolpfade auf
|
||||
- Setzt `PKCS11_PROXY_SOCKET`/`PKCS11_DAEMON_SOCKET`
|
||||
- PATH-Ergaenzung fuer native Abhaengigkeiten
|
||||
- Warmup via `pkcs11-tool -L`
|
||||
|
||||
3. `Pkcs11SessionRunner`
|
||||
- Interop Library-Load mit mehreren Init-Modi (Fallback-Reihenfolge)
|
||||
- Slot-Resolution (`SlotId`/`TokenLabel`)
|
||||
- Session Login/Logout
|
||||
|
||||
4. `Pkcs11ToolBridge`
|
||||
- Fallback via Prozessaufruf von `pkcs11-tool`
|
||||
- Cert read und DigestInfo-Signatur
|
||||
|
||||
5. `Pkcs11StartupSelfTestHostedService`
|
||||
- Fuehrt beim Start Interop-Probe aus
|
||||
- Fallback-Probe bei Interop-Fehler
|
||||
- Schreibt strukturierten Status
|
||||
|
||||
### 4.4 Wichtige Interfaces/DTOs
|
||||
|
||||
- `IPdfDigitalSignatureService`
|
||||
- `SignPdfAsync(...)`, `CheckPdfSignatureAsync(...)`, `ExtractOriginalPdfAsync(...)`
|
||||
- `IPkcs11SelfTestStatusProvider`
|
||||
- `PdfSignOptionsDto`
|
||||
- `UUID`, `Reason`, `Name`, `Location`, `Email`, `Phone`, `CertificationLevel`
|
||||
- `PdfSignatureCheckResultDto` / `PdfSignatureCheckItemDto`
|
||||
- inklusive `BusinessReason`, `UUID`, `OriginalDocumentSha256`
|
||||
- `Pkcs11SelfTestStatusDto`
|
||||
|
||||
---
|
||||
|
||||
## 5. Betriebserfahrungen und Challenges
|
||||
|
||||
### 5.1 Interop vs Tool-Verhalten
|
||||
|
||||
Beobachtetes Muster in der realen Umgebung:
|
||||
|
||||
- `pkcs11-tool` konnte erfolgreich auf Token zugreifen und signieren.
|
||||
- `Pkcs11Interop` scheiterte mit `CKR_FUNCTION_NOT_SUPPORTED` bzw. fehlendem `C_Initialize` Pointer in bestimmten Modi.
|
||||
|
||||
Konsequenz:
|
||||
|
||||
- Architektur wurde auf **interop-first + controlled tool-fallback** erweitert.
|
||||
- Health-Endpunkt differenziert Modus (`interop`, `pkcs11-tool`, `failed`).
|
||||
|
||||
### 5.2 Typische Fehlbilder
|
||||
|
||||
- `CKR_DEVICE_ERROR` bei Socket-/Proxy-Problemen
|
||||
- `CRYPT_PROVIDER_UNAVAILABLE` bei Pfad/ABI/Dependency-Mismatch
|
||||
- `CRYPT_SLOT_NOT_FOUND` bei Slot-/Label-Mismatch
|
||||
- `CKR_PIN_INCORRECT` bei PIN-Fehler
|
||||
- 307 Redirects bei HTTP/HTTPS-Mix in manuellen Tests
|
||||
|
||||
### 5.3 Plattformfallen
|
||||
|
||||
- MSVC vs MinGW Build-Unterschiede fuer Proxy-Client
|
||||
- fehlende Laufzeit-DLLs (`PATH`/Side-by-side)
|
||||
- Architektur-Mismatch x86 vs x64
|
||||
- Daemon-Bindings nicht stabil (random localhost port statt fixed endpoint)
|
||||
|
||||
---
|
||||
|
||||
## 6. Health, Monitoring und Verifikation
|
||||
|
||||
### 6.1 API Health
|
||||
|
||||
- Endpoint: `GET /api/health/crypt`
|
||||
- Liefert:
|
||||
- `success`, `mode`, `summary`, `interopError`, `toolError`, `checkedAtUtc`
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- In Monitoring aufnehmen und bei `mode=pkcs11-tool` ueber laengere Zeit Warnung triggern.
|
||||
|
||||
### 6.2 Smoke-Test (empfohlen vor Release)
|
||||
|
||||
1. `GET /api/health/crypt` -> `success=true`
|
||||
2. `POST /api/PdfSignature/sign` mit Test-PDF
|
||||
3. `POST /api/PdfSignature/is-signed`
|
||||
4. Pruefen:
|
||||
- `signed=true`
|
||||
- `allSignaturesCryptographicallyValid=true`
|
||||
- `UUID`/`OriginalDocumentSha256` korrekt gemappt
|
||||
|
||||
---
|
||||
|
||||
## 7. Sicherheits- und Betriebsrichtlinien
|
||||
|
||||
### 7.1 Geheimnisse
|
||||
|
||||
- PIN, SO-PIN, Root-Creds niemals in Doku/Screenshots/Repo.
|
||||
- Nach Troubleshooting: Credentials rotieren.
|
||||
|
||||
### 7.2 Temporare SSH-Freigaben zwingend rueckbauen
|
||||
|
||||
Wenn SSH temporaer geoeffnet wurde, MUSS danach Rollback erfolgen:
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now ssh && sudo ufw delete allow from 172.24.10.11 to any port 22 proto tcp && sudo ufw status verbose && sudo systemctl status ssh --no-pager
|
||||
```
|
||||
|
||||
Optional harte Bereinigung:
|
||||
|
||||
```bash
|
||||
sudo apt-get purge -y openssh-server && sudo apt-get autoremove -y
|
||||
```
|
||||
|
||||
### 7.3 Schluesselrotation
|
||||
|
||||
- Neue Labels (`...-v2`) anlegen
|
||||
- App umkonfigurieren
|
||||
- Sign/Verify Regression testen
|
||||
- Altmaterial erst nach Freigabe entfernen
|
||||
|
||||
---
|
||||
|
||||
## 8. API-Verhalten bei Signaturmetadaten
|
||||
|
||||
Aktueller Standard:
|
||||
|
||||
- `UUID` kommt aus Sign-Input (nullable string). Wenn ungeltig/leer -> neue Guid wird erzeugt.
|
||||
- `Reason` wird intern angereichert mit strukturiertem Meta (`SFMETA:v1`).
|
||||
- `ApplicationName` ist rein serverseitig aus `appsettings` gesetzt.
|
||||
- `SigningTime` ist rein serverseitig und wird automatisch gesetzt (`UseUtcSigningTime=true` => `UtcNow`, sonst lokale Zeit).
|
||||
- `CertificationLevel` wird im Controller serverseitig gesetzt (`FormFillingAndAnnotations`), nicht vom Client.
|
||||
|
||||
Damit ist das Verhalten deterministisch und manipulationsaermer fuer Integrationsclients.
|
||||
|
||||
---
|
||||
|
||||
## 9. Troubleshooting-Checkliste (praxisnah)
|
||||
|
||||
1. Erreichbarkeit Linux Proxy:
|
||||
- `Test-NetConnection <linux-host> -Port 5657`
|
||||
2. Token sichtbar?
|
||||
- `pkcs11-tool --module <dll> -L -v`
|
||||
3. Zertifikat lesbar?
|
||||
- `--read-object --type cert --label <cert-label>`
|
||||
4. Signatur technisch moeglich?
|
||||
- `--sign -m SHA256-RSA-PKCS ...`
|
||||
5. App-Health check:
|
||||
- `/api/health/crypt`
|
||||
6. App-sign/verify end-to-end:
|
||||
- `/api/PdfSignature/sign` + `/api/PdfSignature/is-signed`
|
||||
|
||||
Wenn Interop scheitert, aber Tool funktioniert:
|
||||
|
||||
- Betrieb bleibt verfuegbar (Fallback),
|
||||
- aber Incident/Bug fuer Interop-Kompatibilitaet erfassen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Ausblick / empfohlene Verbesserungen
|
||||
|
||||
1. Binary Transport statt Base64 bei grossen PDFs in Render/Sign-Pipelines.
|
||||
2. Persistente Metriken fuer Fallback-Quote (`interop` vs `tool`).
|
||||
3. Zertifikatskette/OCSP/CRL Validierung als optionales Policy-Modul.
|
||||
4. CI-Pruefung auf unsichere SQL-Muster (parallel zum Security-Workstream).
|
||||
5. Standardisierte Toast/UX-Rueckmeldungen fuer Signierfehler in UI.
|
||||
|
||||
---
|
||||
|
||||
## 11. Referenzen
|
||||
|
||||
- SoftHSMv2: `https://github.com/softhsm/SoftHSMv2`
|
||||
- OpenSC / pkcs11-tool: `https://github.com/OpenSC/OpenSC/wiki/Using-pkcs11-tool-and-OpenSSL`
|
||||
- PKCS#11 Standard: `https://www.oasis-open.org/committees/pkcs11/`
|
||||
@@ -0,0 +1,77 @@
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using ClientCertLevel = DocumentService.Client.Models.Requests.PdfSignatureCertificationLevel;
|
||||
using ClientCoordUnit = DocumentService.Client.Models.Requests.PdfSignatureCoordinateUnit;
|
||||
using ClientCoordOrigin = DocumentService.Client.Models.Requests.PdfSignatureCoordinateOrigin;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Proxy that forwards PDF signing to the remote DocumentService API via <see cref="IPdfSignatureClient"/>.
|
||||
/// </summary>
|
||||
public sealed class PdfDigitalSignatureService(IPdfSignatureClient signatureClient) : IPdfDigitalSignatureService
|
||||
{
|
||||
public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken);
|
||||
var imageBytes = image is null ? null : await ReadAllBytesAsync(image, cancellationToken);
|
||||
|
||||
using var signed = await signatureClient.SignAsync(pdfBytes, Map(options), imageBytes, cancellationToken);
|
||||
await signed.CopyToAsync(outputPdf, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Signature checking is handled by DocumentService and is not available through this proxy.");
|
||||
|
||||
private static SignPdfOptions Map(PdfSignOptionsDto? o) => new()
|
||||
{
|
||||
DocumentElementId = o?.DocumentElementId ?? 0,
|
||||
Name = o?.Name ?? string.Empty,
|
||||
Reason = o?.Reason,
|
||||
Location = o?.Location,
|
||||
Email = o?.Email,
|
||||
Phone = o?.Phone,
|
||||
ApplicationName = o?.ApplicationName,
|
||||
CertificationLevel = (ClientCertLevel)(o?.CertificationLevel ?? EnvelopeGenerator.Application.Common.Enums.PdfSignatureCertificationLevel.NoCertification),
|
||||
Appearance = o?.Appearance is null ? null : MapAppearance(o.Appearance)
|
||||
};
|
||||
|
||||
private static PdfSignatureAppearance MapAppearance(PdfSignatureAppearanceDto a) => new()
|
||||
{
|
||||
PageNumber = a.PageNumber,
|
||||
Left = a.Left,
|
||||
Bottom = a.Bottom,
|
||||
Width = a.Width,
|
||||
Height = a.Height,
|
||||
ShowLabels = a.ShowLabels,
|
||||
ShowName = a.ShowName,
|
||||
ShowDate = a.ShowDate,
|
||||
ShowReason = a.ShowReason,
|
||||
ShowLocation = a.ShowLocation,
|
||||
ShowDistinguishedName = a.ShowDistinguishedName,
|
||||
DateTimeFormat = a.DateTimeFormat,
|
||||
CoordinateUnit = (ClientCoordUnit)a.CoordinateUnit,
|
||||
CoordinateOrigin = (ClientCoordOrigin)a.CoordinateOrigin,
|
||||
Font = new PdfSignatureFont
|
||||
{
|
||||
FamilyName = a.Font.FamilyName,
|
||||
Size = a.Font.Size,
|
||||
IsBold = a.Font.IsBold,
|
||||
IsItalic = a.Font.IsItalic,
|
||||
IsUnderline = a.Font.IsUnderline,
|
||||
Color = new PdfSignatureColor { R = a.Font.Color.R, G = a.Font.Color.G, B = a.Font.Color.B }
|
||||
}
|
||||
};
|
||||
|
||||
private static async Task<byte[]> ReadAllBytesAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
if (stream is MemoryStream memory && memory.TryGetBuffer(out var buffer))
|
||||
return buffer.AsSpan(0, (int)memory.Length).ToArray();
|
||||
|
||||
using var copy = new MemoryStream();
|
||||
await stream.CopyToAsync(copy, cancellationToken);
|
||||
return copy.ToArray();
|
||||
}
|
||||
}
|
||||
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Doc;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddDocInfrastructure(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IPdfPageImageRenderer, PdfPageImageRenderer>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DevExpress.Document.Processor" Version="25.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
180
EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
Normal file
180
EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using DevExpress.Drawing;
|
||||
using DevExpress.Pdf;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Doc;
|
||||
|
||||
public class PdfPageImageRenderer : IPdfPageImageRenderer
|
||||
{
|
||||
private const float RenderDpi = 110f;
|
||||
|
||||
public Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pdfBytes);
|
||||
|
||||
if (pdfBytes.Length == 0)
|
||||
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
|
||||
|
||||
using var stream = new MemoryStream(pdfBytes, writable: false);
|
||||
using var processor = new PdfDocumentProcessor();
|
||||
processor.LoadDocument(stream);
|
||||
|
||||
var pages = processor.Document.Pages;
|
||||
if (pages.Count == 0)
|
||||
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
|
||||
|
||||
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(RenderDpi);
|
||||
var results = new List<PdfPageImageDto>(pages.Count);
|
||||
|
||||
for (var i = 0; i < pages.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var pageNumber = i + 1;
|
||||
var page = pages[i];
|
||||
var cropBox = page.CropBox;
|
||||
|
||||
using DXBitmap bitmap = processor.CreateDXBitmap(pageNumber, renderOptions);
|
||||
using var imageStream = new MemoryStream();
|
||||
bitmap.Save(imageStream, DXImageFormat.Png);
|
||||
|
||||
var dataUrl = $"data:image/png;base64,{Convert.ToBase64String(imageStream.ToArray())}";
|
||||
|
||||
results.Add(new PdfPageImageDto
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
WidthPt = cropBox.Width,
|
||||
HeightPt = cropBox.Height,
|
||||
WidthPx = bitmap.Width,
|
||||
HeightPx = bitmap.Height,
|
||||
ImageDataUrl = dataUrl
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(results);
|
||||
}
|
||||
|
||||
public Stream ComposeSignaturePreviewPng(ISignatureInkPayload element)
|
||||
{
|
||||
const double sigW = 1.77 * 72;
|
||||
const double sigH = 1.96 * 72;
|
||||
const double imgRatio = 0.52;
|
||||
const double lineH = 11.5;
|
||||
const double bgPad = 3.0;
|
||||
const double padding = 3.0;
|
||||
|
||||
var date = element.ChangedWhen!.Value.ToString("dd.MM.yyyy");
|
||||
var position = string.IsNullOrWhiteSpace(element.Position) ? null : element.Position.Trim();
|
||||
|
||||
var x = bgPad;
|
||||
var y = bgPad;
|
||||
var imgH = sigH * imgRatio;
|
||||
var lineY = y + imgH + 1.0;
|
||||
var textY = lineY + 1.5;
|
||||
var row1Y = textY;
|
||||
var row2Y = row1Y + lineH;
|
||||
var row3Y = position is null ? row2Y : row2Y + lineH;
|
||||
var contentBottom = row3Y + lineH;
|
||||
var pageWidth = sigW + (bgPad * 2);
|
||||
var pageHeight = contentBottom + bgPad;
|
||||
|
||||
var inkBytes = this.EnsureSupportedImageBytes(element.Ink!);
|
||||
|
||||
using var processor = new PdfDocumentProcessor();
|
||||
processor.CreateEmptyDocument();
|
||||
var page = processor.AddNewPage(new PdfRectangle(0, 0, pageWidth, pageHeight));
|
||||
|
||||
using var graphics = processor.CreateGraphics();
|
||||
|
||||
using var bgBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 255, 253, 240));
|
||||
using var linePen = new DXPen(System.Drawing.Color.FromArgb(180, 100, 100, 120), 0.5f);
|
||||
using var nameBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 20, 20, 20));
|
||||
using var infoBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 80, 80, 80));
|
||||
|
||||
var fontBold = new DXFont("Arial", 7.5f, DXFontStyle.Bold);
|
||||
var fontNormal = new DXFont("Arial", 6.5f, DXFontStyle.Regular);
|
||||
|
||||
graphics.FillRectangle(bgBrush, new System.Drawing.RectangleF(0, 0, (float)pageWidth, (float)pageHeight));
|
||||
|
||||
var imageArea = new System.Drawing.RectangleF((float)x, (float)y, (float)sigW, (float)imgH);
|
||||
var imageRect = FitImageContain(imageArea, inkBytes);
|
||||
graphics.DrawImage(inkBytes, imageRect);
|
||||
|
||||
graphics.DrawLine(linePen, (float)(x + 2), (float)lineY, (float)(x + sigW - 2), (float)lineY);
|
||||
|
||||
graphics.DrawString(
|
||||
element.FullName!.Trim(),
|
||||
fontBold,
|
||||
nameBrush,
|
||||
new System.Drawing.RectangleF((float)(x + padding), (float)row1Y, (float)(sigW - (padding * 2)), (float)lineH));
|
||||
|
||||
if (position is not null)
|
||||
{
|
||||
graphics.DrawString(
|
||||
position,
|
||||
fontNormal,
|
||||
infoBrush,
|
||||
new System.Drawing.RectangleF((float)(x + padding), (float)row2Y, (float)(sigW - (padding * 2)), (float)lineH));
|
||||
}
|
||||
|
||||
graphics.DrawString(
|
||||
$"{element.Place!.Trim()}, {date}",
|
||||
fontNormal,
|
||||
infoBrush,
|
||||
new System.Drawing.RectangleF((float)(x + padding), (float)row3Y, (float)(sigW - (padding * 2)), (float)lineH));
|
||||
|
||||
graphics.AddToPageForeground(page, 72, 72);
|
||||
|
||||
using var pdfStream = new MemoryStream();
|
||||
processor.SaveDocument(pdfStream);
|
||||
pdfStream.Position = 0;
|
||||
|
||||
using var rasterProcessor = new PdfDocumentProcessor();
|
||||
rasterProcessor.LoadDocument(pdfStream);
|
||||
|
||||
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(400f);
|
||||
using DXBitmap bitmap = rasterProcessor.CreateDXBitmap(1, renderOptions);
|
||||
var pngStream = new MemoryStream();
|
||||
bitmap.Save(pngStream, DXImageFormat.Png);
|
||||
pngStream.Position = 0;
|
||||
return pngStream;
|
||||
}
|
||||
|
||||
private static System.Drawing.RectangleF FitImageContain(System.Drawing.RectangleF area, byte[] imageBytes)
|
||||
{
|
||||
using var imageStream = new MemoryStream(imageBytes, writable: false);
|
||||
using var image = DXImage.FromStream(imageStream);
|
||||
|
||||
if (image.Width <= 0 || image.Height <= 0)
|
||||
return area;
|
||||
|
||||
var scale = Math.Min(area.Width / image.Width, area.Height / image.Height);
|
||||
var drawW = image.Width * scale;
|
||||
var drawH = image.Height * scale;
|
||||
var drawX = area.X + ((area.Width - drawW) / 2f);
|
||||
var drawY = area.Y + ((area.Height - drawH) / 2f);
|
||||
|
||||
return new System.Drawing.RectangleF(drawX, drawY, drawW, drawH);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsSupportedImageFormat(byte[] bytes)
|
||||
{
|
||||
var isPng = bytes.Length >= 4 &&
|
||||
bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47;
|
||||
if (isPng)
|
||||
return true;
|
||||
|
||||
var isJpeg = bytes.Length >= 3 &&
|
||||
bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF;
|
||||
return isJpeg;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||
|
||||
public enum ConsumerType
|
||||
{
|
||||
API,
|
||||
RabbitMQ
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using DigitalData.MessagingService.RabbitMQ;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||
|
||||
public class NotificationOptions
|
||||
{
|
||||
public const string SectionName = "Notification";
|
||||
|
||||
public ConsumerType ConsumerType { get; set; }
|
||||
|
||||
public string? ApiUrl { get; set; }
|
||||
|
||||
public string SenderEmail { get; set; } = null!;
|
||||
|
||||
public RabbitMqConnectionConfiguration? RabbitMQ { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using DigitalData.MessagingService.Client;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
using EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddNotificationInfrastructure(this IServiceCollection services, IConfiguration configuration, string sectionName = NotificationOptions.SectionName)
|
||||
{
|
||||
var ntfConfiguration = configuration.GetSection(sectionName);
|
||||
services.Configure<NotificationOptions>(ntfConfiguration);
|
||||
|
||||
var options = ntfConfiguration.Get<NotificationOptions>()
|
||||
?? throw new InvalidOperationException(
|
||||
$"Failed to bind configuration section '{sectionName}' to {nameof(NotificationOptions)}. Ensure the section exists and is correctly configured.");
|
||||
|
||||
switch (options.ConsumerType)
|
||||
{
|
||||
case ConsumerType.API:
|
||||
if (options.ApiUrl is string apiUrl)
|
||||
services.AddMessagingServiceHttpClient(opt =>
|
||||
{
|
||||
opt.BaseAddress = new Uri(apiUrl);
|
||||
// Additional configuration for the MessagingServiceHttpClient can be added here if needed
|
||||
});
|
||||
else
|
||||
throw new InvalidOperationException(
|
||||
$"MessagingServiceApiUrl must be provided when ClientType is set to Api in the '{sectionName}' configuration section.");
|
||||
break;
|
||||
case ConsumerType.RabbitMQ:
|
||||
if(options.RabbitMQ is not null)
|
||||
services.AddMessagingServiceClient(ntfConfiguration, nameof(options.RabbitMQ));
|
||||
else
|
||||
throw new InvalidOperationException(
|
||||
$"Missing RabbitMQ connection configuration." +
|
||||
$"When ClientType is set to RabbitMQ, a valid RabbitMQ configuration must be provided in the '{sectionName}' configuration section.");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported ConsumerType '{options.ConsumerType}' in the '{sectionName}' configuration section.");
|
||||
}
|
||||
|
||||
services.AddScoped<IEnvelopeNotificationService, EnvelopeNotificationService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DigitalData.MessagingService.Client" Version="3.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
using DigitalData.MessagingService.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||
|
||||
public static class EnvelopeNotificationExtensions
|
||||
{
|
||||
public static EmailAttachmentDto ToEmailAttachmentDto(this (string FileName, byte[] Content) attachment) => new()
|
||||
{
|
||||
FileName = attachment.FileName,
|
||||
Content = attachment.Content
|
||||
};
|
||||
|
||||
public static IEnumerable<EmailAttachmentDto> ToEmailAttachmentDto(this IEnumerable<(string FileName, byte[] Content)> attachments)
|
||||
=> attachments.Select(ToEmailAttachmentDto);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Exceptions;
|
||||
using EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||
|
||||
public class EnvelopeNotificationService(IRepository<EmailTemplate> TempRepo, IMessagingServiceClient MsgClient, IOptions<NotificationOptions> ntfOpt) : IEnvelopeNotificationService
|
||||
{
|
||||
private readonly NotificationOptions NtfOpt = ntfOpt.Value;
|
||||
|
||||
private async Task<EmailTemplate> GetTemplateAsync(EmailTemplateType templateType, CancellationToken cancel = default)
|
||||
{
|
||||
return await TempRepo.Where(t => t.Name == templateType.ToString()).FirstOrDefaultAsync(cancel)
|
||||
?? throw new DataIntegrityException($"{templateType} email template not found");
|
||||
}
|
||||
|
||||
public async Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
|
||||
where TEnvelopeNotificationBase : EnvelopeNotificationBase
|
||||
{
|
||||
var template = await GetTemplateAsync(notification.GetTemplateType(), cancel);
|
||||
|
||||
// create email command
|
||||
var mail = new PublishEmailViaSmtpCommand()
|
||||
{
|
||||
Body = template.Body.ReplacePlaceholders(notification),
|
||||
Subject = template.Subject.ReplacePlaceholders(notification),
|
||||
Recipients = [receiverEmail],
|
||||
Sender = NtfOpt.SenderEmail.ToSender(),
|
||||
IsHtml = true
|
||||
};
|
||||
|
||||
// add attachments if any
|
||||
if(notification.Attachments?.Any() ?? false)
|
||||
mail = mail.WithAttachments(notification.Attachments.ToEmailAttachmentDto());
|
||||
|
||||
await MsgClient.SendAsync(mail, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||
|
||||
public static class ReplacePlaceholderExtensions
|
||||
{
|
||||
private sealed record PlaceholderProperty(string Placeholder, PropertyInfo Property);
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, PlaceholderProperty[]> PropertyCache = new();
|
||||
|
||||
public static string ReplacePlaceholders(this string template, object data)
|
||||
{
|
||||
var properties = PropertyCache.GetOrAdd(data.GetType(), CreatePlaceholderProperties);
|
||||
|
||||
foreach (var item in properties)
|
||||
{
|
||||
var value = item.Property.GetValue(data)?.ToString() ?? string.Empty;
|
||||
template = template.Replace(
|
||||
item.Placeholder,
|
||||
value,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private static PlaceholderProperty[] CreatePlaceholderProperties(
|
||||
Type type)
|
||||
{
|
||||
return [.. type
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
||||
.Select(p => new PlaceholderProperty(
|
||||
$"[{ToCapitalSnakeCase(p.Name)}]",
|
||||
p))];
|
||||
}
|
||||
|
||||
private static string ToCapitalSnakeCase(string name)
|
||||
{
|
||||
return Regex.Replace(
|
||||
Regex.Replace(name, @"([A-Z]+)([A-Z][a-z])", "$1_$2"),
|
||||
@"([a-z0-9])([A-Z])",
|
||||
"$1_$2").ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
@@ -214,4 +214,4 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Original email template: DocumentAccessCodeReceived (GUID 5)
|
||||
-- Backed up before redesign: 2026-10-29 16:32:15
|
||||
-- Subject: Zugriffscode für Dokument erhalten: ''[DOCUMENT_TITLE]''
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||
<br /><B><I>
|
||||
[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet. <br />
|
||||
<br />
|
||||
Verwenden Sie den folgenden Zugriffscode, um das Dokument einzusehen:<br />
|
||||
<br />
|
||||
[DOCUMENT_ACCESS_CODE]<br />
|
||||
<br />
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
WHERE GUID = 5;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Original email template: DocumentCompleted (GUID 4)
|
||||
-- Backed up before redesign: 2026-10-29 16:29:55
|
||||
-- Subject: Umschlag abgeschlossen: ''[DOCUMENT_TITLE]''
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||
<br />
|
||||
Der Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B> wurde erfolgreich abgeschlossen.<br />
|
||||
<br />
|
||||
Sie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br />
|
||||
<br />
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
WHERE GUID = 4;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Original email template: DocumentDeleted (GUID 2)
|
||||
-- Backed up before redesign: 2026-10-29 16:23:45
|
||||
-- Subject: Umschlag zurückgezogen: ''[DOCUMENT_TITLE]''
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||
<br /><B><I>
|
||||
[NAME_SENDER]</I></B> hat den Umschlag <B><I>''[DOCUMENT_TITLE]''</I></B> gelöscht/zurückgezogen.<br /><p>
|
||||
Begründung: <br /> <I>[REASON]</I> <p>
|
||||
<br />
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Original email template: DocumentReceived (GUID 1, de-DE)
|
||||
-- Backed up before redesign: 2026-10-29 16:06:07
|
||||
-- Subject: Dokument erhalten: ''[DOCUMENT_TITLE]''
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||
<br /><B><I>
|
||||
[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet.<br />
|
||||
<br />
|
||||
Über den folgenden Link können Sie das Dokument einsehen[DOCUMENT_PROCESS]: <a href="[LINK_TO_DOCUMENT]">[LINK_TO_DOCUMENT_TEXT]</a><br />
|
||||
<br />
|
||||
[MESSAGE]<br />
|
||||
<br />
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
WHERE GUID = 1;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Original email template: DocumentRejected_ADM (GUID 6)
|
||||
-- Backed up before redesign: 2026-10-29 16:33:34
|
||||
-- Subject: ''[DOCUMENT_TITLE]'' - Vorgang zurückgezogen
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_SENDER],<p><B><I>[NAME_RECEIVER]</I></B> hat den Umschlag <B><I>''[DOCUMENT_TITLE]''</I></B> mit folgendem Grund abgelehnt: <p>
|
||||
[REASON]
|
||||
<p>Der Umschlag wurde auf den Status Rejected gesetzt. <p>
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
WHERE GUID = 6;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Original email template: DocumentRejected_REC_2 (GUID 10)
|
||||
-- Backed up before redesign: 2026-10-29 16:37:12
|
||||
-- Subject: ''[DOCUMENT_TITLE]'' - Vorgang abgelehnt.
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_RECEIVER],
|
||||
<p>Der Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B> wurde durch einen anderen Vertragspartner abgelehnt.<p> Der Absender <B><I>[NAME_SENDER]</I></B> wird sich bei Bedarf mit Ihnen in Verbindung setzen. <p>
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
WHERE GUID = 10;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Original email template: DocumentRejected_REC (GUID 9)
|
||||
-- Backed up before redesign: 2026-10-29 16:34:21
|
||||
-- Subject: ''[DOCUMENT_TITLE]'' - Bestätigung der Ablehnung
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_RECEIVER],
|
||||
<p>Hiermit bestätigen wir Ihnen die Ablehnung des Vorgangs <B><I>''[DOCUMENT_TITLE]''</I></B>!<p>Der Absender <B><I>[NAME_SENDER]</I></B> wurde über die Ablehnung informiert. <p>
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
WHERE GUID = 9;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Original email template: DocumentShared (GUID 11)
|
||||
-- Backed up before redesign: 2026-10-29 16:42:51
|
||||
-- Subject: Dokument geteilt: ''[DOCUMENT_TITLE]''
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag,<br /> <br /><B><I> [NAME_RECEIVER]</I></B> hat Ihnen ein Dokument zum Ansehen gesendet.<br /> <br /> Über den folgenden Link können Sie das Dokument einsehen: <a href="[LINK_TO_DOCUMENT]">[LINK_TO_DOCUMENT_TEXT]</a><br /> <br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]'
|
||||
WHERE GUID = 11;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Original email template: DocumentSigned (GUID 3)
|
||||
-- Backed up before redesign: 2026-10-29 16:25:45
|
||||
-- Subject: Dokument [RECEIVER_ACTION]: ''[DOCUMENT_TITLE]''
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||
<br />
|
||||
hiermit bestätigen wir Ihnen die erfolgreiche [FINAL_STATUS] für den Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B>.<br />
|
||||
Wenn alle [FINAL_ACTION] haben, erhalten Sie ebenfalls per E-Mail ein abgeschlossenes Exemplar mit entsprechendem Zertifikat!
|
||||
<br />
|
||||
Mit freundlichen Grüßen<br />
|
||||
<br />
|
||||
[NAME_PORTAL]'
|
||||
WHERE GUID = 3;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Original email template: TotpSecret (GUID 12)
|
||||
-- Backed up before redesign: 2026-10-29 16:44:44
|
||||
-- Subject: 2-Faktor-Authentifizierung QR-Code
|
||||
-- Restore by running this script against DD_ECM:
|
||||
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||
SET BODY = N'Guten Tag,<br /> <br />Sie können auf Ihren Zwei-Faktor-Authentifizierungscode zugreifen, indem Sie den unten stehenden QR-Code mit einer beliebigen Authentifizierungs-App auf Ihrem Telefon scannen (Google Authenticator, Microsoft Authenticator usw.). Dieser Code ist bis zum [TFA_EXPIRATION] gültig.<br /> <br /> <img src="data:image/png;base64,[TFA_QR_CODE]" style="width: 13rem; height: 13rem;"><br /> <br />
|
||||
<br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]'
|
||||
WHERE GUID = 12;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user