Compare commits
108 Commits
bugfix/sav
...
feat/wizar
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -370,3 +370,4 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
/ignore/EnvelopeGenerator.Form
|
/ignore/EnvelopeGenerator.Form
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json
|
||||||
|
|||||||
87
AGENTS.md
87
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
|
- 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.
|
**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
|
### Deployment Docs
|
||||||
@@ -63,6 +73,36 @@ 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)
|
## Active Architecture (Post-Migration)
|
||||||
|
|
||||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||||
@@ -238,6 +278,7 @@ Manual testing workflow:
|
|||||||
|
|
||||||
## Technical Debt TODOs
|
## 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.
|
- 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).
|
- 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.
|
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
|
||||||
@@ -250,6 +291,8 @@ Manual testing workflow:
|
|||||||
- 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.
|
- 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.
|
- 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.
|
- 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
|
## Database
|
||||||
|
|
||||||
@@ -261,11 +304,51 @@ Manual testing workflow:
|
|||||||
**Key Tables:**
|
**Key Tables:**
|
||||||
- `TBSIG_ENVELOPE` - Envelope metadata
|
- `TBSIG_ENVELOPE` - Envelope metadata
|
||||||
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
|
- `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_RECEIVER` - Receiver registry
|
||||||
- `TBSIG_DOCUMENT` - PDF binary data
|
- `TBSIG_ENVELOPE_DOCUMENT` - PDF binary data
|
||||||
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
- `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
|
## DevExpress
|
||||||
|
|
||||||
**License:** Commercial (v25.2.3)
|
**License:** Commercial (v25.2.3)
|
||||||
|
|||||||
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,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;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
// TODO: Add the “Reason” property and integrate with UI and the digital signature process.
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocReceiverElementDto : IDocReceiverElement
|
public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the element.
|
/// Gets or sets the unique identifier of the element.
|
||||||
@@ -18,11 +20,21 @@ public class DocReceiverElementDto : IDocReceiverElement
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int DocumentId { get; set; }
|
public int DocumentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public virtual DocumentDto? Document { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the identifier of the receiver.
|
/// Gets or sets the identifier of the receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ReceiverId { get; set; }
|
public int ReceiverId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public virtual ReceiverDto? Receiver { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of the element.
|
/// Gets or sets the type of the element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -122,4 +134,41 @@ public class DocReceiverElementDto : IDocReceiverElement
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? Ink { get; set; }
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ public record DocumentDto : DocumentDetailsDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the binary data of the document, if available.
|
/// Gets or sets the binary data of the document, if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? ByteData { get; init; }
|
public byte[]? ByteData { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default value is 'de-DE'
|
/// Default value is 'de-DE'
|
||||||
@@ -84,7 +84,7 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
@@ -28,7 +30,7 @@ public class EnvelopeTypeDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the certification type identifier.
|
/// Gets or sets the certification type identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether an access code is required.
|
/// Gets or sets a value indicating whether an access code is required.
|
||||||
@@ -78,5 +80,5 @@ public class EnvelopeTypeDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the contract type associated with the envelope type.
|
/// Gets or sets the contract type associated with the envelope type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,8 +1,66 @@
|
|||||||
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
public interface IPdfPageImageRenderer
|
public interface IPdfPageImageRenderer
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pdfBytes"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -96,6 +97,10 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
|||||||
/// <exception cref="NotImplementedException"></exception>
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
|
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 placeHolders = CreatePlaceHolders(notification);
|
||||||
|
|
||||||
var temp = await TempRepo
|
var temp = await TempRepo
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
|
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
|
||||||
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
||||||
|
services.Configure<SignatureFieldParams>(config.GetSection(nameof(SignatureFieldParams)));
|
||||||
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
||||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||||
|
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
|
||||||
|
|
||||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||||
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? Id { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeDocument { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -47,7 +56,10 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
|
|||||||
{
|
{
|
||||||
var q = _repository.Query;
|
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);
|
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
|
||||||
|
|
||||||
if (request.Envelope.Uuid is string envelopeUuid)
|
if (request.Envelope.Uuid is string envelopeUuid)
|
||||||
@@ -59,6 +71,9 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
|
|||||||
if (request.Receiver.Signature is string signature)
|
if (request.Receiver.Signature is string signature)
|
||||||
q = q.Where(e => e.Receiver.Signature == signature);
|
q = q.Where(e => e.Receiver.Signature == signature);
|
||||||
|
|
||||||
|
if( request.IncludeDocument)
|
||||||
|
q = q.Include(e => e.Document);
|
||||||
|
|
||||||
var elements = await q.ToListAsync(cancellationToken);
|
var elements = await q.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using AutoMapper;
|
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
@@ -21,12 +19,10 @@ public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRe
|
|||||||
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
||||||
{
|
{
|
||||||
private readonly IRepository<Document> _repo;
|
private readonly IRepository<Document> _repo;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo, IMapper mapper)
|
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_mapper = mapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
||||||
@@ -36,9 +32,71 @@ public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetai
|
|||||||
if (query.UserId is int userId)
|
if (query.UserId is int userId)
|
||||||
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
return await docQuery
|
// 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)
|
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
||||||
.ProjectTo<DocumentDetailsDto>(_mapper.ConfigurationProvider)
|
.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);
|
.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,
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Befehl zur Erstellung eines Umschlags.
|
/// Befehl zur Erstellung eines Umschlags.
|
||||||
@@ -4,7 +4,7 @@ using MediatR;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the creation of an envelope along with its associated document and recipients.
|
/// 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;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <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()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<Receiver, ReceiverDto>();
|
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>
|
/// </remarks>
|
||||||
public record ReadEnvelopeReceiverSecretQuery
|
public record ReadEnvelopeReceiverSecretQuery
|
||||||
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
|
: 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>
|
/// <summary>
|
||||||
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
|
/// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
|
|||||||
Title = t.Title ?? string.Empty,
|
Title = t.Title ?? string.Empty,
|
||||||
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
||||||
ExpiresDays = t.ExpiresDays,
|
ExpiresDays = t.ExpiresDays,
|
||||||
CertificationType = (int?)t.CertificationType,
|
CertificationType = t.CertificationType,
|
||||||
UseAccessCode = t.UseAccessCode,
|
UseAccessCode = t.UseAccessCode,
|
||||||
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
||||||
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
@@ -35,7 +36,7 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zertifizierungstyp fuer den Signaturprozess.
|
/// Zertifizierungstyp fuer den Signaturprozess.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
||||||
@@ -129,10 +130,27 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
public bool UseSQLExecutor { get; set; } = true;
|
public bool UseSQLExecutor { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="DataAsBase64"></param>
|
||||||
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
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);
|
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(
|
public record CreateEnvelopeReceiverDto(
|
||||||
string EmailAddress,
|
string EmailAddress,
|
||||||
string? Name,
|
string? Name,
|
||||||
|
|||||||
@@ -1,68 +1,66 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
|
||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using DigitalData.UserManager.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
using EnvelopeGenerator.Application.Histories.Commands;
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain;
|
using EnvelopeGenerator.Domain;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </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 IRepository<Document> DocumentRepository => _provider.GetRequiredService<IRepository<Document>>();
|
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => Provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
||||||
|
|
||||||
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => _provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
private IRepository<EnvelopeType> EnvelopeTypeRepository => Provider.GetRequiredService<IRepository<EnvelopeType>>();
|
||||||
|
|
||||||
private IRepository<EnvelopeType> EnvelopeTypeRepository => _provider.GetRequiredService<IRepository<EnvelopeType>>();
|
private IRepository<Receiver> ReceiverRepository => Provider.GetRequiredService<IRepository<Receiver>>();
|
||||||
|
|
||||||
private IRepository<Receiver> ReceiverRepository => _provider.GetRequiredService<IRepository<Receiver>>();
|
private IRepository<DocReceiverElement> DocReceiverElementRepository => Provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
||||||
|
|
||||||
private IRepository<DocReceiverElement> DocReceiverElementRepository => _provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
private IRepository<EmailTemplate> EmailTemplateRepository => Provider.GetRequiredService<IRepository<EmailTemplate>>();
|
||||||
|
|
||||||
private IRepository<EmailTemplate> EmailTemplateRepository => _provider.GetRequiredService<IRepository<EmailTemplate>>();
|
private IRepository<EmailOut> EmailOutRepository => Provider.GetRequiredService<IRepository<EmailOut>>();
|
||||||
|
|
||||||
private IRepository<EmailOut> EmailOutRepository => _provider.GetRequiredService<IRepository<EmailOut>>();
|
private MailParams MailParams => Provider.GetRequiredService<IOptions<MailParams>>().Value;
|
||||||
|
|
||||||
private MailParams MailParams => _provider.GetRequiredService<IOptions<MailParams>>().Value;
|
private DispatcherParams DispatcherParams => Provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
||||||
|
|
||||||
private DispatcherParams DispatcherParams => _provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
private SignatureFieldParams SignatureFieldParams => Provider.GetRequiredService<IOptionsMonitor<SignatureFieldParams>>().CurrentValue;
|
||||||
|
|
||||||
private IConfigService ConfigService => _provider.GetRequiredService<IConfigService>();
|
[Obsolete("Use IConfigService instead.")]
|
||||||
|
private IConfigService ConfigService => Provider.GetRequiredService<IConfigService>();
|
||||||
|
|
||||||
private IMediator Mediator => _provider.GetRequiredService<IMediator>();
|
private IMediator Mediator => Provider.GetRequiredService<IMediator>();
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="provider"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public CreateEnvelopeCommandHandler(IServiceProvider provider, IMapper mapper)
|
|
||||||
{
|
|
||||||
_provider = provider;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -230,7 +228,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
.ThenInclude(er => er.Receiver)
|
.ThenInclude(er => er.Receiver)
|
||||||
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
return _mapper.Map<EnvelopeDto>(envelope);
|
return Mapper.Map<EnvelopeDto>(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
||||||
@@ -268,9 +266,9 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
||||||
UseAccessCode: useAccessCode,
|
UseAccessCode: useAccessCode,
|
||||||
CertificationType: request.CertificationType
|
CertificationType: request.CertificationType
|
||||||
?? (int?)selectedType?.CertificationType
|
?? selectedType?.CertificationType
|
||||||
?? existing?.CertificationType
|
?? existing?.CertificationType
|
||||||
?? (int)CertificationType.AdvancedElectronicSignature,
|
?? CertificationType.AdvancedElectronicSignature,
|
||||||
FinalEmailToCreator: request.FinalEmailToCreator
|
FinalEmailToCreator: request.FinalEmailToCreator
|
||||||
?? (int?)selectedType?.FinalEmailToCreator
|
?? (int?)selectedType?.FinalEmailToCreator
|
||||||
?? existing?.FinalEmailToCreator
|
?? existing?.FinalEmailToCreator
|
||||||
@@ -299,7 +297,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
||||||
ContractType: existing?.ContractType
|
ContractType: existing?.ContractType
|
||||||
?? selectedType?.ContractType
|
?? selectedType?.ContractType
|
||||||
?? (int)ContractType.Contract);
|
?? ContractType.Contract);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
||||||
@@ -320,7 +318,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
int? EnvelopeTypeId,
|
int? EnvelopeTypeId,
|
||||||
string Language,
|
string Language,
|
||||||
bool UseAccessCode,
|
bool UseAccessCode,
|
||||||
int CertificationType,
|
CertificationType CertificationType,
|
||||||
int FinalEmailToCreator,
|
int FinalEmailToCreator,
|
||||||
int FinalEmailToReceivers,
|
int FinalEmailToReceivers,
|
||||||
bool SendReminderEmails,
|
bool SendReminderEmails,
|
||||||
@@ -330,7 +328,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
int ExpiresWarningWhenDays,
|
int ExpiresWarningWhenDays,
|
||||||
DateTime? ExpiresWhen,
|
DateTime? ExpiresWhen,
|
||||||
DateTime? ExpiresWarningWhen,
|
DateTime? ExpiresWarningWhen,
|
||||||
int ContractType);
|
ContractType ContractType);
|
||||||
|
|
||||||
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
@@ -444,8 +442,8 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
ElementType = 1,
|
ElementType = 1,
|
||||||
X = field.X,
|
X = field.X,
|
||||||
Y = field.Y,
|
Y = field.Y,
|
||||||
Width = 1.77,
|
Width = SignatureFieldParams.Width,
|
||||||
Height = 1.96,
|
Height = SignatureFieldParams.Height,
|
||||||
Page = field.Page,
|
Page = field.Page,
|
||||||
Required = true,
|
Required = true,
|
||||||
ReadOnly = false,
|
ReadOnly = false,
|
||||||
@@ -529,15 +527,15 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
if (!hasDocument)
|
if (!hasDocument)
|
||||||
throw new BadRequestException("Cannot send envelope without a document.");
|
throw new BadRequestException("Cannot send envelope without a document.");
|
||||||
|
|
||||||
var links = await EnvelopeReceiverRepository.Query
|
var envRcvs = await EnvelopeReceiverRepository.Query
|
||||||
.Include(er => er.Receiver)
|
.Include(er => er.Receiver)
|
||||||
.Where(er => er.EnvelopeId == envelope.Id)
|
.Where(er => er.EnvelopeId == envelope.Id)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
if (links.Count == 0)
|
if (envRcvs.Count == 0)
|
||||||
throw new BadRequestException("Cannot send envelope without receivers.");
|
throw new BadRequestException("Cannot send envelope without receivers.");
|
||||||
|
|
||||||
var receiverIds = links.Select(l => l.ReceiverId).Distinct().ToList();
|
var receiverIds = envRcvs.Select(l => l.ReceiverId).Distinct().ToList();
|
||||||
var documentIds = await DocumentRepository.Query
|
var documentIds = await DocumentRepository.Query
|
||||||
.Where(d => d.EnvelopeId == envelope.Id)
|
.Where(d => d.EnvelopeId == envelope.Id)
|
||||||
.Select(d => d.Id)
|
.Select(d => d.Id)
|
||||||
@@ -550,7 +548,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
||||||
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
||||||
|
|
||||||
var receiversWithoutFields = links
|
var receiversWithoutFields = envRcvs
|
||||||
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
||||||
.Select(link => link.Name)
|
.Select(link => link.Name)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -574,70 +572,39 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
Status = EnvelopeStatus.EnvelopeQueued
|
Status = EnvelopeStatus.EnvelopeQueued
|
||||||
}, cancel);
|
}, cancel);
|
||||||
|
|
||||||
await QueueInvitationEmailsAsync(envelope, links, userReference, cancel);
|
await SendInvitationEmailsAsync(envelope, envRcvs, cancel);
|
||||||
|
|
||||||
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task QueueInvitationEmailsAsync(
|
private async Task SendInvitationEmailsAsync(
|
||||||
Envelope envelope,
|
Envelope envelope,
|
||||||
IReadOnlyCollection<EnvelopeReceiver> links,
|
IReadOnlyCollection<EnvelopeReceiver> envRcvs,
|
||||||
string senderReference,
|
|
||||||
CancellationToken cancel)
|
CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var template = await EmailTemplateRepository.Query
|
foreach (var er in envRcvs)
|
||||||
.SingleOrDefaultAsync(x => x.Name == EmailTemplateType.DocumentReceived.ToString(), cancel)
|
|
||||||
?? throw new InvalidOperationException($"Email template not found: {EmailTemplateType.DocumentReceived}");
|
|
||||||
|
|
||||||
var signatureHost = await ConfigService.ReadDefaultSignatureHost();
|
|
||||||
var senderName = senderReference;
|
|
||||||
|
|
||||||
foreach (var link in links)
|
|
||||||
{
|
{
|
||||||
var receiver = link.Receiver;
|
var advancedSignature = envelope.CertificationType == CertificationType.AdvancedElectronicSignature;
|
||||||
if (receiver is null || string.IsNullOrWhiteSpace(receiver.EmailAddress) || string.IsNullOrWhiteSpace(receiver.Signature))
|
var origin = new Uri(ReqCntxt.Origin ?? throw new InvalidOperationException($"The request origin is missing in {nameof(CreateEnvelopeCommandHandler)}."));
|
||||||
continue;
|
try
|
||||||
|
|
||||||
var envelopeKey = (envelope.Uuid, receiver.Signature).ToEnvelopeKey();
|
|
||||||
var documentLink = $"{signatureHost.TrimEnd('/')}/envelope/login/{Uri.EscapeDataString(envelopeKey)}";
|
|
||||||
|
|
||||||
var placeholders = new Dictionary<string, string>(MailParams.Placeholders)
|
|
||||||
{
|
{
|
||||||
["[NAME_RECEIVER]"] = link.Name ?? string.Empty,
|
await EnvNtfService.Notify(er.Receiver.EmailAddress, new DocumentReceivedNotification
|
||||||
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
{
|
||||||
["[DOCUMENT_TITLE]"] = envelope.Title ?? string.Empty,
|
AdvancedSignature = advancedSignature,
|
||||||
["[MESSAGE]"] = envelope.Message ?? string.Empty,
|
DocumentTitle = envelope.Title ?? string.Empty,
|
||||||
["[LINK_TO_DOCUMENT]"] = documentLink,
|
NameReceiver = er.Name ?? string.Empty,
|
||||||
["[LINK_TO_DOCUMENT_TEXT]"] = documentLink[..Math.Min(40, documentLink.Length)] + ".."
|
EnvelopeUuid = envelope.Uuid,
|
||||||
};
|
IsReadAndConfirm = envelope.IsReadAndConfirm(),
|
||||||
|
Message = envelope.Message ?? string.Empty,
|
||||||
var subject = ReplacePlaceHolders(template.Subject, placeholders);
|
NameSender = $"{envelope.User.Prename} {envelope.User.Name}",
|
||||||
var body = ReplacePlaceHolders(template.Body, placeholders);
|
Origin = origin,
|
||||||
|
ReceiverSignature = er.Receiver.Signature,
|
||||||
var emailOut = new EmailOut
|
}, cancel);
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
{
|
{
|
||||||
EmailAddress = receiver.EmailAddress,
|
Logger.LogError(ex, "Failed to send invitation email for receiver {emailAddress} in envelope {envelopeId}.", er.Receiver.EmailAddress, envelope.Id);
|
||||||
EmailSubj = subject,
|
}
|
||||||
EmailBody = body,
|
|
||||||
AddedWhen = DateTime.Now,
|
|
||||||
AddedWho = DispatcherParams.AddedWho,
|
|
||||||
SendingProfile = DispatcherParams.SendingProfile,
|
|
||||||
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
|
||||||
EmailAttmt1 = DispatcherParams.EmailAttmt1,
|
|
||||||
WfId = (int)EnvelopeStatus.MessageInvitationSent,
|
|
||||||
ReferenceString = receiver.EmailAddress,
|
|
||||||
ReferenceId = link.ReceiverId
|
|
||||||
};
|
|
||||||
|
|
||||||
await EmailOutRepository.CreateAsync(emailOut, cancel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ReplacePlaceHolders(string text, Dictionary<string, string> placeHolders)
|
|
||||||
{
|
|
||||||
foreach (var placeHolder in placeHolders)
|
|
||||||
text = text.Replace(placeHolder.Key, placeHolder.Value);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
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 AutoMapper;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
@@ -14,6 +17,11 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeDocAndDocRes { get; init; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -148,6 +156,86 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
if (request is { OnlyCompleted: true })
|
if (request is { OnlyCompleted: true })
|
||||||
query = query.Where(e => Status.Completed.Contains(e.Status));
|
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
|
var envelopes = await query
|
||||||
.Include(e => e.Type)
|
.Include(e => e.Type)
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
|
|||||||
@@ -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">
|
<data name="Back" xml:space="preserve">
|
||||||
<value>Zurück</value>
|
<value>Zurück</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Next" xml:space="preserve">
|
||||||
|
<value>Weiter</value>
|
||||||
|
</data>
|
||||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||||
<value>Klicken Sie auf den</value>
|
<value>Klicken Sie auf den</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -564,6 +567,12 @@
|
|||||||
<data name="AddReceiver" xml:space="preserve">
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
<value>Empfänger hinzufügen</value>
|
<value>Empfänger hinzufügen</value>
|
||||||
</data>
|
</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">
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
<value>Empfänger löschen</value>
|
<value>Empfänger löschen</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -123,6 +123,9 @@
|
|||||||
<data name="Back" xml:space="preserve">
|
<data name="Back" xml:space="preserve">
|
||||||
<value>Back</value>
|
<value>Back</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Next" xml:space="preserve">
|
||||||
|
<value>Next</value>
|
||||||
|
</data>
|
||||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||||
<value>Click the</value>
|
<value>Click the</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -564,6 +567,12 @@
|
|||||||
<data name="AddReceiver" xml:space="preserve">
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
<value>Add Receiver</value>
|
<value>Add Receiver</value>
|
||||||
</data>
|
</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">
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
<value>Delete Receiver</value>
|
<value>Delete Receiver</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -123,6 +123,9 @@
|
|||||||
<data name="Back" xml:space="preserve">
|
<data name="Back" xml:space="preserve">
|
||||||
<value>Retour</value>
|
<value>Retour</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Next" xml:space="preserve">
|
||||||
|
<value>Suivant</value>
|
||||||
|
</data>
|
||||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||||
<value>Cliquez sur</value>
|
<value>Cliquez sur</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -564,6 +567,12 @@
|
|||||||
<data name="AddReceiver" xml:space="preserve">
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
<value>Ajouter un destinataire</value>
|
<value>Ajouter un destinataire</value>
|
||||||
</data>
|
</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">
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
<value>Supprimer le destinataire</value>
|
<value>Supprimer le destinataire</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
{
|
{
|
||||||
public enum CertificationType
|
public enum CertificationType
|
||||||
{
|
{
|
||||||
AdvancedElectronicSignature = 1
|
AdvancedElectronicSignature = 1,
|
||||||
// ElectronicSignature = 1
|
ElectronicSignature = 2
|
||||||
// QualifiedSignature = 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
{
|
{
|
||||||
public static bool IsActive(this EnvelopeStatus status)
|
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)
|
public static bool IsCompleted(this EnvelopeStatus status)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using System.Collections.Generic;
|
|||||||
namespace EnvelopeGenerator.Domain.Entities
|
namespace EnvelopeGenerator.Domain.Entities
|
||||||
{
|
{
|
||||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
||||||
public class DocReceiverElement : IDocReceiverElement, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
public class DocReceiverElement : IDocReceiverElement, ISignatureInkPayload, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
||||||
{
|
{
|
||||||
public DocReceiverElement()
|
public DocReceiverElement()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
SendReminderEmails = false;
|
SendReminderEmails = false;
|
||||||
FirstReminderDays = 0;
|
FirstReminderDays = 0;
|
||||||
ReminderIntervalDays = 0;
|
ReminderIntervalDays = 0;
|
||||||
CertificationType = (int)Constants.CertificationType.AdvancedElectronicSignature;
|
CertificationType = Constants.CertificationType.AdvancedElectronicSignature;
|
||||||
UseAccessCode = false;
|
UseAccessCode = false;
|
||||||
Documents = Enumerable.Empty<Document>().ToList();
|
Documents = Enumerable.Empty<Document>().ToList();
|
||||||
Histories = Enumerable.Empty<History>().ToList();
|
Histories = Enumerable.Empty<History>().ToList();
|
||||||
@@ -83,7 +83,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public string Comment { get; set; }
|
public string Comment { get; set; }
|
||||||
|
|
||||||
[Column("CONTRACT_TYPE")]
|
[Column("CONTRACT_TYPE")]
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
@@ -111,7 +111,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public bool ReadOnly => this.IsReadAndConfirm();
|
public bool ReadOnly => this.IsReadAndConfirm();
|
||||||
|
|
||||||
[Column("CERTIFICATION_TYPE")]
|
[Column("CERTIFICATION_TYPE")]
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
[Column("USE_ACCESS_CODE")]
|
[Column("USE_ACCESS_CODE")]
|
||||||
public bool? UseAccessCode { get; set; }
|
public bool? UseAccessCode { get; set; }
|
||||||
@@ -129,7 +129,11 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public int? ExpiresWarningWhenDays { get; set; }
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
[ForeignKey("UserId")]
|
[ForeignKey("UserId")]
|
||||||
public User User { get; set; }
|
public User
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
User { get; set; }
|
||||||
|
|
||||||
[Column("TFA_ENABLED")]
|
[Column("TFA_ENABLED")]
|
||||||
public bool? TfaEnabled { get; set; }
|
public bool? TfaEnabled { get; set; }
|
||||||
@@ -137,7 +141,6 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
= false;
|
= false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
[NotMapped]
|
|
||||||
[Column("DOC_RESULT")]
|
[Column("DOC_RESULT")]
|
||||||
public byte[]
|
public byte[]
|
||||||
#if nullable
|
#if nullable
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
#endif
|
#endif
|
||||||
@@ -56,7 +58,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public int? ReminderIntervalDays { get; set; }
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
[Column("CONTRACT_TYPE")]
|
[Column("CONTRACT_TYPE")]
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool TFA_Enabled { get; set; } = false;
|
public bool TFA_Enabled { get; set; } = false;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net462;net7.0;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>net462;net7.0;net8.0;net9.0</TargetFrameworks>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
<AssemblyVersion>1.4.0</AssemblyVersion>
|
||||||
<FileVersion>1.4.0</FileVersion>
|
<FileVersion>1.4.0</FileVersion>
|
||||||
</PropertyGroup>
|
</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
|
public interface IEnvelope
|
||||||
{
|
{
|
||||||
int? EnvelopeTypeId { get; set; }
|
int? EnvelopeTypeId { get; set; }
|
||||||
|
|
||||||
|
byte[]
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
DocResult { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class EnvelopeExtensions
|
public static class EnvelopeExtensions
|
||||||
@@ -16,5 +22,10 @@
|
|||||||
{
|
{
|
||||||
return envelope.EnvelopeTypeId != 2;
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -276,4 +276,61 @@
|
|||||||
<data name="YesWithAttachment" xml:space="preserve">
|
<data name="YesWithAttachment" xml:space="preserve">
|
||||||
<value>Yes, with Attachment</value>
|
<value>Yes, with Attachment</value>
|
||||||
</data>
|
</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>
|
||||||
|
|||||||
@@ -113,4 +113,61 @@
|
|||||||
<data name="MessageInvitationSent" xml:space="preserve">
|
<data name="MessageInvitationSent" xml:space="preserve">
|
||||||
<value>Invitation envoyee</value>
|
<value>Invitation envoyee</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|||||||
@@ -279,4 +279,61 @@
|
|||||||
<data name="YesWithAttachment" xml:space="preserve">
|
<data name="YesWithAttachment" xml:space="preserve">
|
||||||
<value>Ja, mit Anhang</value>
|
<value>Ja, mit Anhang</value>
|
||||||
</data>
|
</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 If
|
||||||
End Sub
|
End Sub
|
||||||
Private Function TranslateCertificationType(pType As CertificationType) As String
|
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
|
End Function
|
||||||
Private Function TranslateFinalEmailType(pType As FinalEmailType) As String
|
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
|
End Function
|
||||||
|
|
||||||
Private Sub btOK_Click(sender As Object, e As EventArgs) Handles btOK.Click
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using DevExpress.Drawing;
|
using DevExpress.Drawing;
|
||||||
using DevExpress.Pdf;
|
using DevExpress.Pdf;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure.Doc;
|
namespace EnvelopeGenerator.Infrastructure.Doc;
|
||||||
|
|
||||||
@@ -54,4 +56,125 @@ public class PdfPageImageRenderer : IPdfPageImageRenderer
|
|||||||
|
|
||||||
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(results);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Components.Sender;
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeWizardResult(
|
||||||
|
string Title,
|
||||||
|
string Message,
|
||||||
|
int? EnvelopeTypeId,
|
||||||
|
string Language,
|
||||||
|
bool UseAccessCode,
|
||||||
|
bool TFAEnabled,
|
||||||
|
CertificationType CertificationType,
|
||||||
|
int FinalEmailToCreator,
|
||||||
|
int FinalEmailToReceivers,
|
||||||
|
bool SendReminderEmails,
|
||||||
|
int FirstReminderDays,
|
||||||
|
int ReminderIntervalDays,
|
||||||
|
int ExpiresWhenDays,
|
||||||
|
int ExpiresWarningWhenDays,
|
||||||
|
IReadOnlyList<CreateEnvelopeWizardReceiver> Receivers,
|
||||||
|
byte[]? DocumentBytes,
|
||||||
|
string? DocumentFileName);
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeWizardReceiver(string FullName, string EmailAddress);
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
|
<div class="sender-pdf-dropzone @DropZoneClass">
|
||||||
|
<div class="sender-pdf-dropzone__content">
|
||||||
|
<div class="sender-pdf-dropzone__title">@CurrentActionText</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(DragDropHintText))
|
||||||
|
{
|
||||||
|
<div class="sender-pdf-dropzone__hint">@DragDropHintText</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputFile OnChange="OnFileSelectedAsync"
|
||||||
|
accept="@Accept"
|
||||||
|
disabled="@Disabled"
|
||||||
|
@ondragenter="OnDragEnter"
|
||||||
|
@ondragleave="OnDragLeave"
|
||||||
|
@ondragover="OnDragOver"
|
||||||
|
@ondrop="OnDrop"
|
||||||
|
class="sender-pdf-dropzone__input" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool HasDocument { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Accept { get; set; } = ".pdf";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string UploadText { get; set; } = "PDF hochladen";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ReplaceText { get; set; } = "PDF ersetzen";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string DragDropHintText { get; set; } = "Datei hierhin ziehen oder klicken";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<InputFileChangeEventArgs> FileSelected { get; set; }
|
||||||
|
|
||||||
|
private int _dragDepth;
|
||||||
|
private bool _isDragActive;
|
||||||
|
|
||||||
|
private string CurrentActionText => HasDocument ? ReplaceText : UploadText;
|
||||||
|
|
||||||
|
private string DropZoneClass
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return "sender-pdf-dropzone--disabled";
|
||||||
|
|
||||||
|
return _isDragActive ? "sender-pdf-dropzone--drag-active" : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragEnter(DragEventArgs _)
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dragDepth++;
|
||||||
|
_isDragActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragLeave(DragEventArgs _)
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dragDepth = Math.Max(0, _dragDepth - 1);
|
||||||
|
if (_dragDepth == 0)
|
||||||
|
_isDragActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragOver(DragEventArgs _)
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDragActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrop(DragEventArgs _)
|
||||||
|
{
|
||||||
|
_dragDepth = 0;
|
||||||
|
_isDragActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFileSelectedAsync(InputFileChangeEventArgs args)
|
||||||
|
{
|
||||||
|
_dragDepth = 0;
|
||||||
|
_isDragActive = false;
|
||||||
|
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (FileSelected.HasDelegate)
|
||||||
|
await FileSelected.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter;
|
||||||
|
|
||||||
|
public readonly record struct ReceiverFilterCriteria
|
||||||
|
{
|
||||||
|
public readonly string FullName;
|
||||||
|
public readonly string EmailAddress;
|
||||||
|
|
||||||
|
public ReceiverFilterCriteria(string? fullName = "", string? emailAddress = "")
|
||||||
|
{
|
||||||
|
FullName = Normalize(fullName);
|
||||||
|
EmailAddress = Normalize(emailAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Matches(ReceiverFilterCriteria criteria)
|
||||||
|
=> FullName.Contains(criteria.FullName, StringComparison.Ordinal)
|
||||||
|
&& EmailAddress.Contains(criteria.EmailAddress, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static string Normalize(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return string.Concat(text.Where(c => !char.IsWhiteSpace(c))).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter;
|
||||||
|
|
||||||
|
public class ReceiverSuggestionFilterHandler(IEnumerable<RecentReceiverSuggestionDto> suggestions)
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<Entry> _entries = suggestions
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.FullName) && !string.IsNullOrWhiteSpace(x.EmailAddress))
|
||||||
|
.Select(x => new Entry(new ReceiverFilterCriteria(x.FullName, x.EmailAddress), x))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private readonly record struct Entry(ReceiverFilterCriteria Criteria, RecentReceiverSuggestionDto Suggestion);
|
||||||
|
|
||||||
|
public IEnumerable<RecentReceiverSuggestionDto> Filter(ReceiverFilterCriteria criteria)
|
||||||
|
{
|
||||||
|
return _entries
|
||||||
|
.Where(x => x.Criteria.Matches(criteria))
|
||||||
|
.Select(x => x.Suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<RecentReceiverSuggestionDto> Filter(string fullName, string emailAddress)
|
||||||
|
{
|
||||||
|
var criteria = new ReceiverFilterCriteria(fullName, emailAddress);
|
||||||
|
return Filter(criteria);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MudBlazor" Version="8.0.0" />
|
||||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
_selected = option;
|
_selected = option;
|
||||||
_isOpen = false;
|
_isOpen = false;
|
||||||
await LocalizationService.SetLanguageAsync(option.Code);
|
await LocalizationService.SetLanguageAsync(option.Code);
|
||||||
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
|
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</main>
|
</main>
|
||||||
<footer class="receiver-footer">
|
<footer class="receiver-footer">
|
||||||
<div class="receiver-footer__left">
|
<div class="receiver-footer__left">
|
||||||
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
<span>© signFLOW 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||||
<span class="receiver-footer__sep">|</span>
|
<span class="receiver-footer__sep">|</span>
|
||||||
<a href="@GetPrivacyPolicyPath()" target="_blank" rel="noopener">@L("PrivacyPolicy", "Datenschutz")</a>
|
<a href="@GetPrivacyPolicyPath()" target="_blank" rel="noopener">@L("PrivacyPolicy", "Datenschutz")</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<h1 class="home-hero-header__title">SignFlow</h1>
|
<h1 class="home-hero-header__title">signFLOW</h1>
|
||||||
<p class="home-hero-header__subtitle">@L("Index_Subtitle", "Willkommen im eSign-Portal")</p>
|
<p class="home-hero-header__subtitle">@L("Index_Subtitle", "Willkommen im eSign-Portal")</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@
|
|||||||
@using EnvelopeGenerator.Application.Envelopes.Commands
|
@using EnvelopeGenerator.Application.Envelopes.Commands
|
||||||
@using EnvelopeGenerator.Application.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
@using EnvelopeGenerator.Domain.Constants
|
@using EnvelopeGenerator.Domain.Constants
|
||||||
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
|
@using EnvelopeGenerator.Application.Common.Configurations
|
||||||
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@using Microsoft.AspNetCore.WebUtilities
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using Microsoft.Extensions.Options
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@@ -18,10 +20,13 @@
|
|||||||
@inject DocumentService DocumentService
|
@inject DocumentService DocumentService
|
||||||
@inject PdfRenderService PdfRenderService
|
@inject PdfRenderService PdfRenderService
|
||||||
@inject ReceiverService ReceiverService
|
@inject ReceiverService ReceiverService
|
||||||
|
@inject EnvelopeReceiverService EnvelopeReceiverService
|
||||||
@inject EnvelopeService EnvelopeService
|
@inject EnvelopeService EnvelopeService
|
||||||
@inject EnvelopeTypeService EnvelopeTypeService
|
@inject EnvelopeTypeService EnvelopeTypeService
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject IStringLocalizer<Resource> UiLocalizer
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
@inject IStringLocalizer<Model> ModelLocalizer
|
||||||
|
@inject IOptionsMonitor<SignatureFieldParams> SignatureFieldOptions
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
||||||
@@ -32,64 +37,52 @@
|
|||||||
|
|
||||||
@* ── Action Bar ── *@
|
@* ── Action Bar ── *@
|
||||||
<div class="envelope-action-bar">
|
<div class="envelope-action-bar">
|
||||||
<div class="envelope-action-bar__inner"
|
<div class="envelope-action-bar__inner sender-editor-action-inner">
|
||||||
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
|
||||||
|
|
||||||
@* Left: Title + meta inputs + receivers *@
|
@* Left: Title + meta inputs + receivers *@
|
||||||
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem;">
|
<div class="sender-editor-main-column">
|
||||||
|
|
||||||
@* ── Title & Message inputs ── *@
|
@* ── Title & Message inputs ── *@
|
||||||
<div style="display: flex; align-items: flex-start; gap: 0.5rem; flex-wrap: wrap;">
|
<div class="sender-editor-meta-row">
|
||||||
|
|
||||||
@* Titel *@
|
@* Titel *@
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 1 1 180px; min-width: 140px;">
|
<div class="sender-editor-input-field sender-editor-input-field--title">
|
||||||
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
<label class="sender-editor-label">
|
||||||
text-transform: uppercase; letter-spacing: 0.05em;">
|
@L("Title", "Titel") <span class="sender-editor-required-asterisk">*</span>
|
||||||
@L("Title", "Titel") <span style="color:#dc2626;">*</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value="@_envelopeTitle"
|
value="@_envelopeTitle"
|
||||||
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
|
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
|
||||||
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
style="font-size: 0.78rem; font-weight: 500; color: #1f2937;
|
class="sender-editor-text-input @(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "sender-editor-text-input--invalid" : string.Empty)" />
|
||||||
border: 1px solid #d1d5db; border-radius: 5px;
|
|
||||||
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
@(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "border-color:#dc2626;" : "")" />
|
|
||||||
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
|
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
|
||||||
{
|
{
|
||||||
<span style="font-size:0.6rem;color:#dc2626;">@L("RequiredField", "Pflichtfeld")</span>
|
<span class="sender-editor-required-text">@L("RequiredField", "Pflichtfeld")</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Nachricht *@
|
@* Nachricht *@
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 2 1 260px; min-width: 180px;">
|
<div class="sender-editor-input-field sender-editor-input-field--message">
|
||||||
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
<label class="sender-editor-label">
|
||||||
text-transform: uppercase; letter-spacing: 0.05em;">
|
@L("Message", "Nachricht") <span class="sender-editor-optional">(@L("Optional", "optional"))</span>
|
||||||
@L("Message", "Nachricht") <span style="color:#6b7280;font-weight:400;">(@L("Optional", "optional"))</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value="@_envelopeMessage"
|
value="@_envelopeMessage"
|
||||||
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
|
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
|
||||||
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
style="font-size: 0.78rem; color: #1f2937;
|
class="sender-editor-text-input" />
|
||||||
border: 1px solid #d1d5db; border-radius: 5px;
|
|
||||||
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
|
||||||
background: #fff;" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* PDF filename badge *@
|
@* PDF filename badge *@
|
||||||
@if (_pdfLoaded)
|
@if (_pdfLoaded)
|
||||||
{
|
{
|
||||||
<div style="display:flex;align-items:flex-end;gap:0.4rem;padding-bottom:0.05rem;flex-shrink:0;">
|
<div class="sender-editor-file-meta">
|
||||||
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
<span class="sender-editor-file-name">@_fileName</span>
|
||||||
@if (_signatureFields.Count > 0)
|
@if (_signatureFields.Count > 0)
|
||||||
{
|
{
|
||||||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
<span class="sender-editor-signature-count">
|
||||||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
|
||||||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
|
||||||
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
|
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -97,122 +90,145 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details style="border:1px solid #dbe5ef;border-radius:8px;background:#f9fbfd;">
|
<div class="sender-settings-shell">
|
||||||
<summary style="cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.7rem;font-size:0.74rem;font-weight:700;color:#1f2937;text-transform:uppercase;letter-spacing:0.04em;">
|
<DxAccordion ExpandMode="AccordionExpandMode.SingleOrNone" CssClass="sender-settings-accordion">
|
||||||
<span>@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")</span>
|
<Items>
|
||||||
<span style="font-size:0.66rem;color:#64748b;font-weight:600;">@L("SenderEditor_AdvancedOptional", "Erweitert")</span>
|
<DxAccordionItem Text='@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")' Expanded="false">
|
||||||
</summary>
|
<ContentTemplate>
|
||||||
|
<div class="sender-settings-body">
|
||||||
|
<div class="sender-settings-grid">
|
||||||
|
<div class="sender-settings-field">
|
||||||
|
<label class="sender-settings-label">@L("EnvelopeType", "Umschlagtyp")</label>
|
||||||
|
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
||||||
|
@onchange="OnEnvelopeTypeChanged"
|
||||||
|
class="sender-settings-select">
|
||||||
|
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
|
||||||
|
@foreach (var type in _envelopeTypes)
|
||||||
|
{
|
||||||
|
<option value="@type.Id">@type.Title</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="padding:0 0.7rem 0.7rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:0.45rem;">
|
<div class="sender-settings-field">
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<label class="sender-settings-label">@L("Language", "Sprache")</label>
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("EnvelopeType", "Umschlagtyp")</label>
|
<select @bind="_language" class="sender-settings-select">
|
||||||
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
<option value="de">Deutsch (de)</option>
|
||||||
@onchange="OnEnvelopeTypeChanged"
|
<option value="en">English (en)</option>
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
<option value="de-DE">Deutsch (de-DE)</option>
|
||||||
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
|
<option value="en-US">English (en-US)</option>
|
||||||
@foreach (var type in _envelopeTypes)
|
</select>
|
||||||
{
|
</div>
|
||||||
<option value="@type.Id">@type.Title</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("Language", "Sprache")</label>
|
<label class="sender-settings-label">@L("CertificationType", "Zertifizierung")</label>
|
||||||
<select @bind="_language"
|
<select @bind="_certificationType" class="sender-settings-select">
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
<option value="@CertificationType.AdvancedElectronicSignature">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
||||||
<option value="de">Deutsch (de)</option>
|
<option value="@CertificationType.ElectronicSignature">@L("ElectronicSignature", "Electronic Signature")</option>
|
||||||
<option value="en">English (en)</option>
|
</select>
|
||||||
<option value="de-DE">Deutsch (de-DE)</option>
|
</div>
|
||||||
<option value="en-US">English (en-US)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-toggles">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("CertificationType", "Zertifizierung")</label>
|
<label class="sender-settings-toggle">
|
||||||
<select @bind="_certificationType"
|
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
@L("TFAEnabled", "2FA aktiviert")
|
||||||
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
</label>
|
||||||
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
|
<label class="sender-settings-toggle">
|
||||||
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
|
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
||||||
</select>
|
@L("UseAccessCode", "Zugriffscode verwenden")
|
||||||
</div>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
<label class="sender-settings-label">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
||||||
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
<select @bind="_finalEmailToCreator" class="sender-settings-select">
|
||||||
@L("TFAEnabled", "2FA aktiviert")
|
<option value="0">@L("No", "Nein")</option>
|
||||||
</label>
|
<option value="1">@L("Yes", "Ja")</option>
|
||||||
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
||||||
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
</select>
|
||||||
@L("UseAccessCode", "Zugriffscode verwenden")
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
<label class="sender-settings-label">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
||||||
<select @bind="_finalEmailToCreator"
|
<select @bind="_finalEmailToReceivers" class="sender-settings-select">
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
<option value="0">@L("No", "Nein")</option>
|
||||||
<option value="0">@L("No", "Nein")</option>
|
<option value="1">@L("Yes", "Ja")</option>
|
||||||
<option value="1">@L("Yes", "Ja")</option>
|
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
||||||
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-toggles sender-settings-toggles--single">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
<label class="sender-settings-toggle">
|
||||||
<select @bind="_finalEmailToReceivers"
|
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
||||||
<option value="0">@L("No", "Nein")</option>
|
</label>
|
||||||
<option value="1">@L("Yes", "Ja")</option>
|
</div>
|
||||||
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
<label class="sender-settings-label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
||||||
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
|
||||||
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
<label class="sender-settings-label">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
||||||
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
<label class="sender-settings-label">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
||||||
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm sender-settings-number" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
<label class="sender-settings-label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
||||||
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
|
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm sender-settings-number" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
</div>
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
</ContentTemplate>
|
||||||
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
|
</DxAccordionItem>
|
||||||
</div>
|
</Items>
|
||||||
</div>
|
</DxAccordion>
|
||||||
</details>
|
</div>
|
||||||
|
|
||||||
<div class="sender-receivers-panel">
|
<div class="sender-receivers-panel">
|
||||||
<div class="sender-receivers-panel__header">
|
<div class="sender-receivers-list">
|
||||||
<div class="sender-receivers-panel__title-wrap">
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-receivers-list__add-btn"
|
||||||
<span class="sender-receivers-panel__title">@L("Receiver", "Empfänger")</span>
|
|
||||||
<span class="sender-receivers-panel__count">@_receivers.Count</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn"
|
|
||||||
@onclick="OpenAddReceiverPopup"
|
@onclick="OpenAddReceiverPopup"
|
||||||
title='@L("AddReceiver", "Empfänger hinzufügen")'>
|
title='@L("AddReceiver", "Empfänger hinzufügen")'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
|
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="pdf-toolbar__btn-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
|
<span class="sender-receivers-list__add-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
|
||||||
|
<span class="badge rounded-pill text-bg-primary sender-receivers-list__count">@_receivers.Count</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@foreach (var receiver in _receivers)
|
||||||
|
{
|
||||||
|
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
|
||||||
|
<div class="sender-receiver-chip__body">
|
||||||
|
<div class="sender-receiver-chip__email" title="@receiver.FullName">@receiver.Email</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__remove-btn"
|
||||||
|
@onclick="() => RemoveReceiverAsync(receiver)"
|
||||||
|
title='@L("Delete", "Loeschen")'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__place-btn
|
||||||
|
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
|
||||||
|
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
||||||
|
style="--receiver-color:@receiver.Color;"
|
||||||
|
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (_receivers.Count == 0)
|
@if (_receivers.Count == 0)
|
||||||
@@ -221,41 +237,11 @@
|
|||||||
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="sender-receivers-list">
|
|
||||||
@foreach (var receiver in _receivers)
|
|
||||||
{
|
|
||||||
<div class="sender-receiver-chip"
|
|
||||||
style="border-left: 3px solid @receiver.Color;">
|
|
||||||
<div class="sender-receiver-chip__body">
|
|
||||||
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
|
||||||
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(receiver.PhoneNumber))
|
|
||||||
{
|
|
||||||
<div class="sender-receiver-chip__phone">@receiver.PhoneNumber</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact
|
|
||||||
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
|
|
||||||
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
|
||||||
style="@(_pendingReceiverForPlacement?.Id == receiver.Id ? $"background:{receiver.Color};color:#fff;border-color:{receiver.Color};" : $"color:{receiver.Color};border-color:{receiver.Color};")"
|
|
||||||
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
|
||||||
</svg>
|
|
||||||
<span class="pdf-toolbar__btn-text">@L("SenderEditor_AddSignature", "Signatur hinzufügen")</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Right: Buttons *@
|
@* Right: Buttons *@
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
|
<div class="sender-editor-actions">
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||||||
@onclick="Cancel"
|
@onclick="Cancel"
|
||||||
@@ -264,14 +250,12 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
@* PDF Upload *@
|
@* PDF Upload *@
|
||||||
<label class="pdf-toolbar__btn"
|
<label class="pdf-toolbar__btn sender-editor-upload-btn @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-upload-btn--disabled" : string.Empty)"
|
||||||
title='@L("UploadPdf", "PDF hochladen")'
|
title='@L("UploadPdf", "PDF hochladen")'
|
||||||
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
|
>
|
||||||
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.6");
|
|
||||||
display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.75rem; font-size: 0.75rem; font-weight: 500;">
|
|
||||||
@if (_isPdfUploadInProgress)
|
@if (_isPdfUploadInProgress)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
<span class="spinner-border spinner-border-sm sender-editor-spinner" role="status"></span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -299,7 +283,7 @@
|
|||||||
<InputFile OnChange="OnPdfFileSelectedAsync"
|
<InputFile OnChange="OnPdfFileSelectedAsync"
|
||||||
accept=".pdf"
|
accept=".pdf"
|
||||||
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
||||||
style="display: none;" />
|
class="sender-editor-hidden-input" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@if (_pdfLoaded && _signatureFields.Count > 0)
|
@if (_pdfLoaded && _signatureFields.Count > 0)
|
||||||
@@ -320,8 +304,7 @@
|
|||||||
title='@L("Save", "Speichern")'>
|
title='@L("Save", "Speichern")'>
|
||||||
@if (_isSaving)
|
@if (_isSaving)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-1" role="status"
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
||||||
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -336,12 +319,10 @@
|
|||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
||||||
@onclick="SendAsync"
|
@onclick="SendAsync"
|
||||||
disabled="@_isSaving"
|
disabled="@_isSaving"
|
||||||
title='@L("SendEnvelope", "Umschlag senden")'
|
title='@L("SendEnvelope", "Umschlag senden")'>
|
||||||
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
|
|
||||||
@if (_isSaving)
|
@if (_isSaving)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-1" role="status"
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
||||||
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -357,11 +338,9 @@
|
|||||||
@* Placement mode hint bar *@
|
@* Placement mode hint bar *@
|
||||||
@if (_pendingReceiverForPlacement is not null)
|
@if (_pendingReceiverForPlacement is not null)
|
||||||
{
|
{
|
||||||
<div style="background: @_pendingReceiverForPlacement.Color; color: white; font-size: 0.75rem; font-weight: 500;
|
<div class="sender-editor-placement-hint" style="--placement-color:@_pendingReceiverForPlacement.Color;">
|
||||||
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
|
||||||
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
||||||
<button @onclick="CancelPlacement"
|
<button @onclick="CancelPlacement" class="sender-editor-placement-cancel">
|
||||||
style="background:none;border:none;color:white;cursor:pointer;font-size:0.75rem;text-decoration:underline;padding:0;">
|
|
||||||
@L("Cancel", "Abbrechen")
|
@L("Cancel", "Abbrechen")
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,30 +348,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── Content ── *@
|
@* ── Content ── *@
|
||||||
<div class="envelope-content" style="padding: 0; overflow: hidden;">
|
<div class="envelope-content sender-editor-content">
|
||||||
@if (!_pdfLoaded)
|
@if (!_pdfLoaded)
|
||||||
{
|
{
|
||||||
@* Empty state *@
|
@* Empty state *@
|
||||||
<div class="d-flex justify-content-center align-items-center h-100">
|
<div class="d-flex justify-content-center align-items-center h-100">
|
||||||
<div class="text-center" style="max-width: 420px;">
|
<div class="text-center sender-editor-empty-state">
|
||||||
<div style="width: 72px; height: 72px; background: rgba(255,255,255,0.15);
|
<div class="sender-editor-empty-state__icon-wrap">
|
||||||
border-radius: 50%; display: flex; align-items: center;
|
|
||||||
justify-content: center; margin: 0 auto 1.25rem;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
||||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
|
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
|
||||||
<p class="text-white mb-4" style="opacity: 0.75; font-size: 0.85rem;">
|
<p class="text-white mb-4 sender-editor-empty-state__description">
|
||||||
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
|
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
|
||||||
</p>
|
</p>
|
||||||
<label class="btn btn-light btn-sm"
|
<label class="btn btn-light btn-sm sender-editor-empty-state__upload @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-empty-state__upload--disabled" : string.Empty)">
|
||||||
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
|
|
||||||
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.8");
|
|
||||||
font-weight: 500;">
|
|
||||||
@if (_isPdfUploadInProgress)
|
@if (_isPdfUploadInProgress)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-1" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -420,7 +394,7 @@
|
|||||||
<InputFile OnChange="OnPdfFileSelectedAsync"
|
<InputFile OnChange="OnPdfFileSelectedAsync"
|
||||||
accept=".pdf"
|
accept=".pdf"
|
||||||
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
||||||
style="display: none;" />
|
class="sender-editor-hidden-input" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -544,18 +518,17 @@
|
|||||||
<BodyContentTemplate>
|
<BodyContentTemplate>
|
||||||
@if (_saveErrorMessage is null)
|
@if (_saveErrorMessage is null)
|
||||||
{
|
{
|
||||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
<div class="sender-editor-save-result">
|
||||||
<div style="flex-shrink:0;width:40px;height:40px;background:#d1fae5;border-radius:50%;
|
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--success">
|
||||||
display:flex;align-items:center;justify-content:center;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
||||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
<p class="sender-editor-save-result__title">
|
||||||
@_saveSuccessTitle
|
@_saveSuccessTitle
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;">
|
<p class="sender-editor-save-result__message">
|
||||||
@_saveSuccessBody
|
@_saveSuccessBody
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -563,19 +536,18 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
<div class="sender-editor-save-result">
|
||||||
<div style="flex-shrink:0;width:40px;height:40px;background:#fee2e2;border-radius:50%;
|
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--error">
|
||||||
display:flex;align-items:center;justify-content:center;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
<p class="sender-editor-save-result__title">
|
||||||
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
|
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
|
<p class="sender-editor-save-result__message sender-editor-save-result__message--break">
|
||||||
@_saveErrorMessage
|
@_saveErrorMessage
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -583,30 +555,25 @@
|
|||||||
}
|
}
|
||||||
</BodyContentTemplate>
|
</BodyContentTemplate>
|
||||||
<FooterContentTemplate>
|
<FooterContentTemplate>
|
||||||
<div style="display:flex;gap:0.5rem;justify-content:flex-end;width:100%;padding:0.5rem 0;">
|
<div class="sender-editor-save-actions">
|
||||||
@if (_saveErrorMessage is null)
|
@if (_saveErrorMessage is null)
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-secondary"
|
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
|
||||||
@onclick="() => _savePopupVisible = false"
|
@onclick="() => _savePopupVisible = false">
|
||||||
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
|
||||||
@L("Close", "Schließen")
|
@L("Close", "Schließen")
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (_saveNavigateToDashboard)
|
@if (_saveNavigateToDashboard)
|
||||||
{
|
{
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary sender-editor-save-actions__primary" @onclick="GoToDashboard">
|
||||||
@onclick="GoToDashboard"
|
|
||||||
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
|
|
||||||
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
|
|
||||||
@L("SenderEditor_ToOverview", "Zur Übersicht")
|
@L("SenderEditor_ToOverview", "Zur Übersicht")
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-secondary"
|
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
|
||||||
@onclick="() => _savePopupVisible = false"
|
@onclick="() => _savePopupVisible = false">
|
||||||
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
|
||||||
@L("Close", "Schließen")
|
@L("Close", "Schließen")
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -618,10 +585,11 @@
|
|||||||
[SupplyParameterFromQuery(Name = "envelopeId")]
|
[SupplyParameterFromQuery(Name = "envelopeId")]
|
||||||
public int? EnvelopeId { get; set; }
|
public int? EnvelopeId { get; set; }
|
||||||
|
|
||||||
// ── Constants ──
|
// ── Signature field size (configurable) ──
|
||||||
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
double SigWidthInches => SignatureFieldOptions.CurrentValue.Width;
|
||||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
double SigHeightInches => SignatureFieldOptions.CurrentValue.Height;
|
||||||
const double SigHeightPt = 1.96 * 72; // 141.12 pt
|
double SigWidthPt => SigWidthInches * 72.0;
|
||||||
|
double SigHeightPt => SigHeightInches * 72.0;
|
||||||
|
|
||||||
// CssClass for JS click coordinate helper
|
// CssClass for JS click coordinate helper
|
||||||
const string ViewerCssClass = "sender-editor-pdf-viewer";
|
const string ViewerCssClass = "sender-editor-pdf-viewer";
|
||||||
@@ -636,7 +604,6 @@
|
|||||||
int _pageCount;
|
int _pageCount;
|
||||||
List<PdfPageImageDto> _pagePreviews = [];
|
List<PdfPageImageDto> _pagePreviews = [];
|
||||||
int? _loadedEnvelopeId;
|
int? _loadedEnvelopeId;
|
||||||
int? _editingEnvelopeId;
|
|
||||||
bool _isLoadingEnvelopeForEdit;
|
bool _isLoadingEnvelopeForEdit;
|
||||||
|
|
||||||
List<SignatureFieldDraft> _signatureFields = [];
|
List<SignatureFieldDraft> _signatureFields = [];
|
||||||
@@ -669,7 +636,7 @@
|
|||||||
string _language = "de-DE";
|
string _language = "de-DE";
|
||||||
bool _useAccessCode;
|
bool _useAccessCode;
|
||||||
bool _tfaEnabled;
|
bool _tfaEnabled;
|
||||||
int _certificationType = (int)CertificationType.AdvancedElectronicSignature;
|
CertificationType _certificationType = CertificationType.AdvancedElectronicSignature;
|
||||||
int _finalEmailToCreator = (int)FinalEmailType.No;
|
int _finalEmailToCreator = (int)FinalEmailType.No;
|
||||||
int _finalEmailToReceivers = (int)FinalEmailType.No;
|
int _finalEmailToReceivers = (int)FinalEmailType.No;
|
||||||
bool _sendReminderEmails;
|
bool _sendReminderEmails;
|
||||||
@@ -691,8 +658,6 @@
|
|||||||
|
|
||||||
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
||||||
|
|
||||||
bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0;
|
|
||||||
|
|
||||||
// ── Lifecycle ──
|
// ── Lifecycle ──
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -707,12 +672,6 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
||||||
|
|
||||||
if (!IsEditMode && _selectedEnvelopeTypeId is null && _envelopeTypes.Count > 0)
|
|
||||||
{
|
|
||||||
_selectedEnvelopeTypeId = _envelopeTypes[0].Id;
|
|
||||||
ApplyEnvelopeTypeDefaults(_envelopeTypes[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -723,28 +682,23 @@
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
|
if (!EnvelopeId.HasValue || EnvelopeId.Value <= 0)
|
||||||
_editingEnvelopeId = EnvelopeId.Value;
|
|
||||||
|
|
||||||
if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
|
|
||||||
{
|
{
|
||||||
await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
|
NavigationManager.NavigateTo("/sender", replace: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsEditMode)
|
if (_loadedEnvelopeId != EnvelopeId && !_isLoadingEnvelopeForEdit)
|
||||||
return;
|
await LoadEnvelopeForEditAsync(EnvelopeId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
_isPageReady = true;
|
_isPageReady = true;
|
||||||
StateHasChanged();
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
||||||
@@ -772,7 +726,7 @@
|
|||||||
if (_tfaEnabled)
|
if (_tfaEnabled)
|
||||||
_useAccessCode = true;
|
_useAccessCode = true;
|
||||||
|
|
||||||
_certificationType = envelope.CertificationType ?? (int)CertificationType.AdvancedElectronicSignature;
|
_certificationType = envelope.CertificationType ?? CertificationType.AdvancedElectronicSignature;
|
||||||
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
|
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
|
||||||
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
|
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
|
||||||
|
|
||||||
@@ -788,12 +742,17 @@
|
|||||||
_receivers = envelopeReceivers
|
_receivers = envelopeReceivers
|
||||||
.Select((er, index) => new ReceiverDraft(
|
.Select((er, index) => new ReceiverDraft(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
er.ReceiverId,
|
||||||
er.Name ?? er.Receiver?.EmailAddress ?? $"Receiver {er.ReceiverId}",
|
er.Name ?? er.Receiver?.EmailAddress ?? $"Receiver {er.ReceiverId}",
|
||||||
er.Receiver?.EmailAddress ?? string.Empty,
|
er.Receiver?.EmailAddress ?? string.Empty,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
ReceiverPalette[index % ReceiverPalette.Length]))
|
ReceiverPalette[index % ReceiverPalette.Length]))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var receiverDraftByReceiverId = _receivers
|
||||||
|
.Where(r => r.ReceiverId.HasValue)
|
||||||
|
.ToDictionary(r => r.ReceiverId!.Value, r => r);
|
||||||
|
|
||||||
var receiverById = envelopeReceivers.ToDictionary(er => er.ReceiverId, er => er);
|
var receiverById = envelopeReceivers.ToDictionary(er => er.ReceiverId, er => er);
|
||||||
var colorByEmail = _receivers
|
var colorByEmail = _receivers
|
||||||
.Where(r => !string.IsNullOrWhiteSpace(r.Email))
|
.Where(r => !string.IsNullOrWhiteSpace(r.Email))
|
||||||
@@ -815,10 +774,17 @@
|
|||||||
? receiverColor
|
? receiverColor
|
||||||
: ReceiverPalette[Math.Abs(element.ReceiverId) % ReceiverPalette.Length];
|
: ReceiverPalette[Math.Abs(element.ReceiverId) % ReceiverPalette.Length];
|
||||||
|
|
||||||
|
var receiverDraft = receiverDraftByReceiverId.TryGetValue(element.ReceiverId, out var draft)
|
||||||
|
? draft
|
||||||
|
: _receivers.FirstOrDefault(r =>
|
||||||
|
(!string.IsNullOrWhiteSpace(receiverEmail) && string.Equals(r.Email, receiverEmail, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(r.FullName, receiverName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
return new SignatureFieldDraft(
|
return new SignatureFieldDraft(
|
||||||
XPt: element.X * 72.0,
|
XPt: element.X * 72.0,
|
||||||
YPt: element.Y * 72.0,
|
YPt: element.Y * 72.0,
|
||||||
Page: element.Page,
|
Page: element.Page,
|
||||||
|
ReceiverDraftId: receiverDraft?.Id ?? Guid.Empty,
|
||||||
ReceiverName: receiverName,
|
ReceiverName: receiverName,
|
||||||
Color: color);
|
Color: color);
|
||||||
})
|
})
|
||||||
@@ -854,7 +820,6 @@
|
|||||||
_fileName = $"Envelope-{envelopeId}.pdf";
|
_fileName = $"Envelope-{envelopeId}.pdf";
|
||||||
_pendingReceiverForPlacement = null;
|
_pendingReceiverForPlacement = null;
|
||||||
_loadedEnvelopeId = envelopeId;
|
_loadedEnvelopeId = envelopeId;
|
||||||
_editingEnvelopeId = envelopeId;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -1054,6 +1019,7 @@
|
|||||||
XPt: xPt,
|
XPt: xPt,
|
||||||
YPt: yPt,
|
YPt: yPt,
|
||||||
Page: page1Based,
|
Page: page1Based,
|
||||||
|
ReceiverDraftId: _pendingReceiverForPlacement.Id,
|
||||||
ReceiverName: _pendingReceiverForPlacement.FullName,
|
ReceiverName: _pendingReceiverForPlacement.FullName,
|
||||||
Color: _pendingReceiverForPlacement.Color);
|
Color: _pendingReceiverForPlacement.Color);
|
||||||
|
|
||||||
@@ -1137,12 +1103,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async Task SaveDraftAsync()
|
async Task SaveDraftAsync()
|
||||||
|
{
|
||||||
|
await SaveDraftCoreAsync(navigateToDashboard: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
|
||||||
{
|
{
|
||||||
if (!ValidateSettings(out var settingsError))
|
if (!ValidateSettings(out var settingsError))
|
||||||
{
|
{
|
||||||
_saveErrorMessage = settingsError;
|
_saveErrorMessage = settingsError;
|
||||||
_savePopupVisible = true;
|
_savePopupVisible = true;
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
@@ -1151,9 +1122,8 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var previousEnvelopeId = EnvelopeId;
|
|
||||||
var command = BuildCreateEnvelopeCommand(send: false);
|
var command = BuildCreateEnvelopeCommand(send: false);
|
||||||
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId} IsEditMode={IsEditMode}", command.EnvelopeId, IsEditMode);
|
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId}", command.EnvelopeId);
|
||||||
var result = await EnvelopeService.UpsertAsync(command);
|
var result = await EnvelopeService.UpsertAsync(command);
|
||||||
|
|
||||||
if (result is null)
|
if (result is null)
|
||||||
@@ -1161,24 +1131,23 @@
|
|||||||
|
|
||||||
EnvelopeId = result.Id;
|
EnvelopeId = result.Id;
|
||||||
_loadedEnvelopeId = result.Id;
|
_loadedEnvelopeId = result.Id;
|
||||||
_editingEnvelopeId = result.Id;
|
|
||||||
|
|
||||||
if (previousEnvelopeId != result.Id)
|
if (navigateToDashboard)
|
||||||
{
|
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
||||||
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
return result.Id;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
||||||
_saveErrorMessage = ex.Message;
|
_saveErrorMessage = ex.Message;
|
||||||
_savePopupVisible = true;
|
_savePopupVisible = true;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1221,7 +1190,7 @@
|
|||||||
|
|
||||||
// Warn if any receiver has no signature field, but don't block
|
// Warn if any receiver has no signature field, but don't block
|
||||||
var receiversWithoutField = _receivers
|
var receiversWithoutField = _receivers
|
||||||
.Where(r => !_signatureFields.Any(f => f.ReceiverName == r.FullName))
|
.Where(r => !_signatureFields.Any(f => f.ReceiverDraftId == r.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
if (receiversWithoutField.Count > 0)
|
if (receiversWithoutField.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1236,21 +1205,10 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var envelopeId = _editingEnvelopeId
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
||||||
?? EnvelopeId
|
|
||||||
?? _loadedEnvelopeId
|
|
||||||
?? GetEnvelopeIdFromCurrentUri();
|
|
||||||
|
|
||||||
if (!envelopeId.HasValue || envelopeId.Value <= 0)
|
if (!envelopeId.HasValue || envelopeId.Value <= 0)
|
||||||
{
|
throw new InvalidOperationException("Envelope id is missing for send.");
|
||||||
var saveResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: false))
|
|
||||||
?? throw new InvalidOperationException("Draft save did not return a result.");
|
|
||||||
|
|
||||||
envelopeId = saveResult.Id;
|
|
||||||
EnvelopeId = saveResult.Id;
|
|
||||||
_loadedEnvelopeId = saveResult.Id;
|
|
||||||
_editingEnvelopeId = saveResult.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId })
|
var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId })
|
||||||
?? throw new InvalidOperationException("Send did not return a result.");
|
?? throw new InvalidOperationException("Send did not return a result.");
|
||||||
@@ -1281,13 +1239,10 @@
|
|||||||
{
|
{
|
||||||
NormalizeSettings();
|
NormalizeSettings();
|
||||||
|
|
||||||
var envelopeId = _editingEnvelopeId
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
||||||
?? EnvelopeId
|
|
||||||
?? _loadedEnvelopeId
|
|
||||||
?? GetEnvelopeIdFromCurrentUri();
|
|
||||||
|
|
||||||
if (IsEditMode && envelopeId is null)
|
if (envelopeId is null)
|
||||||
throw new InvalidOperationException("Edit mode envelope id is missing.");
|
throw new InvalidOperationException("Envelope id is missing.");
|
||||||
|
|
||||||
var docBase64 = _originalPdfBytes is { Length: > 0 }
|
var docBase64 = _originalPdfBytes is { Length: > 0 }
|
||||||
? Convert.ToBase64String(_originalPdfBytes)
|
? Convert.ToBase64String(_originalPdfBytes)
|
||||||
@@ -1296,7 +1251,7 @@
|
|||||||
var receiverCommands = _receivers.Select(receiver =>
|
var receiverCommands = _receivers.Select(receiver =>
|
||||||
{
|
{
|
||||||
var fields = _signatureFields
|
var fields = _signatureFields
|
||||||
.Where(f => f.ReceiverName == receiver.FullName)
|
.Where(f => f.ReceiverDraftId == receiver.Id)
|
||||||
.Select(f => new CreateEnvelopeFieldDto(
|
.Select(f => new CreateEnvelopeFieldDto(
|
||||||
X: f.XPt / 72.0,
|
X: f.XPt / 72.0,
|
||||||
Y: f.YPt / 72.0,
|
Y: f.YPt / 72.0,
|
||||||
@@ -1375,28 +1330,6 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? GetEnvelopeIdFromCurrentUri()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
|
|
||||||
if (string.IsNullOrWhiteSpace(uri.Query))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
|
||||||
if (!query.TryGetValue("envelopeId", out var raw))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return int.TryParse(raw.ToString(), out var id) && id > 0
|
|
||||||
? id
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Receiver popup ──
|
// ── Receiver popup ──
|
||||||
void OpenAddReceiverPopup()
|
void OpenAddReceiverPopup()
|
||||||
{
|
{
|
||||||
@@ -1558,18 +1491,49 @@
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email, phoneNumber,
|
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), null, fullName, email, phoneNumber,
|
||||||
ReceiverPalette[_receivers.Count % ReceiverPalette.Length]));
|
ReceiverPalette[_receivers.Count % ReceiverPalette.Length]));
|
||||||
CloseAddReceiverPopup();
|
CloseAddReceiverPopup();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task RemoveReceiverAsync(ReceiverDraft receiver)
|
||||||
|
{
|
||||||
|
if (_isSaving)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
||||||
|
|
||||||
|
if (envelopeId is int persistedEnvelopeId && persistedEnvelopeId > 0 && receiver.ReceiverId is int persistedReceiverId && persistedReceiverId > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnvelopeReceiverService.DeleteAsync(persistedEnvelopeId, persistedReceiverId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "[SenderEditor] Failed to remove receiver {ReceiverId} from envelope {EnvelopeId}", persistedReceiverId, persistedEnvelopeId);
|
||||||
|
_saveErrorMessage = string.Format(
|
||||||
|
L("SenderEditor_RemoveReceiverError", "Empfaenger konnte nicht entfernt werden: {0}"),
|
||||||
|
ex.Message);
|
||||||
|
_savePopupVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_receivers.RemoveAll(r => r.Id == receiver.Id);
|
||||||
|
_signatureFields.RemoveAll(f => f.ReceiverDraftId == receiver.Id);
|
||||||
|
|
||||||
|
if (_pendingReceiverForPlacement?.Id == receiver.Id)
|
||||||
|
_pendingReceiverForPlacement = null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Models ──
|
// ── Models ──
|
||||||
record SignatureFieldDraft(double XPt, double YPt, int Page, string ReceiverName, string Color);
|
record SignatureFieldDraft(double XPt, double YPt, int Page, Guid ReceiverDraftId, string ReceiverName, string Color);
|
||||||
|
|
||||||
record NormalisedCoords(double NormX, double NormY, int PageIndex);
|
record NormalisedCoords(double NormX, double NormY, int PageIndex);
|
||||||
|
|
||||||
record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber, string Color);
|
record ReceiverDraft(Guid Id, int? ReceiverId, string FullName, string Email, string PhoneNumber, string Color);
|
||||||
|
|
||||||
// ── Receiver colour palette (cycles when > 8 receivers) ──
|
// ── Receiver colour palette (cycles when > 8 receivers) ──
|
||||||
static readonly string[] ReceiverPalette =
|
static readonly string[] ReceiverPalette =
|
||||||
@@ -1586,7 +1550,11 @@
|
|||||||
|
|
||||||
string L(string key, string fallback)
|
string L(string key, string fallback)
|
||||||
{
|
{
|
||||||
var value = UiLocalizer[key];
|
var modelValue = ModelLocalizer[key];
|
||||||
return value.ResourceNotFound ? fallback : value.Value;
|
if (!modelValue.ResourceNotFound)
|
||||||
|
return modelValue.Value;
|
||||||
|
|
||||||
|
var uiValue = UiLocalizer[key];
|
||||||
|
return uiValue.ResourceNotFound ? fallback : uiValue.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,22 @@
|
|||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@using EnvelopeGenerator.Application.Common.Dto
|
@using EnvelopeGenerator.Application.Common.Dto
|
||||||
@using EnvelopeGenerator.Application.Common.Dto.History
|
@using EnvelopeGenerator.Application.Common.Dto.History
|
||||||
|
@using EnvelopeGenerator.Application.Envelopes.Commands
|
||||||
@using EnvelopeGenerator.Application.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
@using EnvelopeGenerator.Domain.Resources
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
|
@using EnvelopeGenerator.Server.Client.Components.Sender
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService
|
@inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService
|
@inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService EnvelopeTypeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||||
@inject ILogger<EnvelopeSenderPage> Logger
|
@inject EnvelopeGenerator.Server.Client.Services.PdfSignatureService PdfSignatureService
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||||
|
@inject ILogger<SenderPage> Logger
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@@ -311,12 +316,13 @@
|
|||||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||||
{
|
{
|
||||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||||
@{
|
@{
|
||||||
var isRejected = IsReceiverRejected(envelope, receiver);
|
var isRejected = IsReceiverRejected(envelope, receiver);
|
||||||
var isSigned = IsReceiverSigned(envelope, receiver);
|
var isSigned = IsReceiverSigned(envelope, receiver);
|
||||||
var accessCode = receiver.AccessCode;
|
var accessCode = receiver.AccessCode;
|
||||||
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
||||||
}
|
var hasReceiverEnvelopeUrl = CanOpenReceiverEnvelopeUrl(envelope, receiver);
|
||||||
|
}
|
||||||
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
||||||
@if (isRejected)
|
@if (isRejected)
|
||||||
{
|
{
|
||||||
@@ -345,27 +351,40 @@
|
|||||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sender-access-code">
|
<div class="sender-access-code">
|
||||||
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
||||||
<div class="sender-access-code__row">
|
<div class="sender-access-code__row">
|
||||||
<input class="sender-access-code__value"
|
<input class="sender-access-code__value"
|
||||||
value="@(hasAccessCode ? accessCode : "-")"
|
value="@(hasAccessCode ? accessCode : "-")"
|
||||||
readonly
|
readonly
|
||||||
title="@L("AccessCode", "Access code")" />
|
title="@L("AccessCode", "Access code")" />
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="sender-access-code__copy"
|
class="sender-access-code__copy"
|
||||||
disabled="@(!hasAccessCode)"
|
disabled="@(!hasAccessCode)"
|
||||||
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
||||||
title="@L("Copy", "Copy")">
|
title="@L("Copy", "Copy")">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
@if (_canUseReceiverEnvelopeUrl)
|
||||||
</div>
|
{
|
||||||
</div>
|
<button type="button"
|
||||||
}
|
class="sender-access-code__copy"
|
||||||
</div>
|
disabled="@(!hasReceiverEnvelopeUrl)"
|
||||||
|
@onclick='() => OpenReceiverEnvelopeUrlAsync(envelope, receiver)'
|
||||||
|
title="@L("Open", "Open")">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M6.364 13.5a1.5 1.5 0 0 1-1.5-1.5V4a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 1 0v4a1.5 1.5 0 0 1-1.5 1.5h-8z"/>
|
||||||
|
<path fill-rule="evenodd" d="M8.5 1a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V2.707L8.854 7.354a.5.5 0 1 1-.708-.708L12.793 2H9a.5.5 0 0 1-.5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -414,9 +433,14 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CreateEnvelopeWizardPopup Visible="@_createWizardVisible"
|
||||||
|
VisibleChanged="OnCreateWizardVisibleChangedAsync"
|
||||||
|
EnvelopeTypes="@_envelopeTypes"
|
||||||
|
StartEditorRequested="StartEditorFromWizardAsync" />
|
||||||
|
|
||||||
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
||||||
<DxPopup @bind-Visible="@_previewVisible"
|
<DxPopup @bind-Visible="@_previewVisible"
|
||||||
HeaderText="@($"{L("Preview", "Vorschau")} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
HeaderText="@($"{(_activeTab == "completed" && _selectedEnvelope?.Status == EnvelopeStatus.EnvelopeCompletelySigned ? L("SignedPdf", "Signiertes PDF") : L("Preview", "Vorschau"))} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
||||||
Width="80vw"
|
Width="80vw"
|
||||||
MaxWidth="80vw"
|
MaxWidth="80vw"
|
||||||
Height="90vh"
|
Height="90vh"
|
||||||
@@ -535,6 +559,7 @@
|
|||||||
private string _activeTab = "active";
|
private string _activeTab = "active";
|
||||||
private bool _isLoading = true;
|
private bool _isLoading = true;
|
||||||
private bool _isLoggingOut = false;
|
private bool _isLoggingOut = false;
|
||||||
|
private bool _canUseReceiverEnvelopeUrl = false;
|
||||||
private string? _errorMessage;
|
private string? _errorMessage;
|
||||||
private DxGrid? _grid;
|
private DxGrid? _grid;
|
||||||
|
|
||||||
@@ -567,6 +592,9 @@
|
|||||||
private string _deleteReason = string.Empty;
|
private string _deleteReason = string.Empty;
|
||||||
private string? _deleteErrorMessage;
|
private string? _deleteErrorMessage;
|
||||||
|
|
||||||
|
private bool _createWizardVisible = false;
|
||||||
|
private IReadOnlyList<EnvelopeTypeDto> _envelopeTypes = [];
|
||||||
|
|
||||||
// Stable keys used to store/restore each tab's layout in UserPreferences.GridLayouts
|
// Stable keys used to store/restore each tab's layout in UserPreferences.GridLayouts
|
||||||
private const string GridKeyActive = "sender.active-envelopes";
|
private const string GridKeyActive = "sender.active-envelopes";
|
||||||
private const string GridKeyCompleted = "sender.completed-envelopes";
|
private const string GridKeyCompleted = "sender.completed-envelopes";
|
||||||
@@ -591,11 +619,23 @@
|
|||||||
try { await ThemeService.LoadFromServerAsync(); }
|
try { await ThemeService.LoadFromServerAsync(); }
|
||||||
catch { /* swallow — default theme remains active */ }
|
catch { /* swallow — default theme remains active */ }
|
||||||
|
|
||||||
|
_canUseReceiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlFeatureEnabledAsync();
|
||||||
|
|
||||||
// Load logo config — authenticated, so this should succeed
|
// Load logo config — authenticated, so this should succeed
|
||||||
var logos = await CustomImagesService.GetAsync();
|
var logos = await CustomImagesService.GetAsync();
|
||||||
_appLogo = logos.App;
|
_appLogo = logos.App;
|
||||||
_companyLogo = logos.Company;
|
_companyLogo = logos.Company;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to load envelope types for create wizard.");
|
||||||
|
_envelopeTypes = [];
|
||||||
|
}
|
||||||
|
|
||||||
await LoadEnvelopesAsync();
|
await LoadEnvelopesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +647,7 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
_allEnvelopes = await EnvelopeService.GetAsync(includeDocAndDocRes: false) ?? [];
|
||||||
|
|
||||||
// Split into active and completed based on effective status
|
// Split into active and completed based on effective status
|
||||||
// (history-aware: rejected envelopes should not stay in active tab).
|
// (history-aware: rejected envelopes should not stay in active tab).
|
||||||
@@ -693,7 +733,55 @@
|
|||||||
|
|
||||||
void CreateEnvelope()
|
void CreateEnvelope()
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo("/sender/editor");
|
_createWizardVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnCreateWizardVisibleChangedAsync(bool visible)
|
||||||
|
{
|
||||||
|
_createWizardVisible = visible;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task StartEditorFromWizardAsync(CreateEnvelopeWizardResult result)
|
||||||
|
{
|
||||||
|
var docBase64 = result.DocumentBytes is { Length: > 0 }
|
||||||
|
? Convert.ToBase64String(result.DocumentBytes)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var command = new CreateEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeTypeId = result.EnvelopeTypeId,
|
||||||
|
Title = result.Title,
|
||||||
|
Message = result.Message,
|
||||||
|
Language = string.IsNullOrWhiteSpace(result.Language) ? "de-DE" : result.Language,
|
||||||
|
UseAccessCode = result.UseAccessCode,
|
||||||
|
TFAEnabled = result.TFAEnabled,
|
||||||
|
CertificationType = result.CertificationType,
|
||||||
|
FinalEmailToCreator = result.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = result.FinalEmailToReceivers,
|
||||||
|
SendReminderEmails = result.SendReminderEmails,
|
||||||
|
FirstReminderDays = result.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = result.ReminderIntervalDays,
|
||||||
|
ExpiresWhenDays = result.ExpiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays = result.ExpiresWarningWhenDays,
|
||||||
|
Send = false,
|
||||||
|
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
|
||||||
|
Receivers = result.Receivers
|
||||||
|
.Select(x => new CreateEnvelopeReceiverDto(x.EmailAddress, x.FullName, null, []))
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var envelope = await EnvelopeService.UpsertAsync(command)
|
||||||
|
?? throw new InvalidOperationException("Envelope creation returned no result.");
|
||||||
|
Navigation.NavigateTo($"/sender/editor?envelopeId={envelope.Id}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Failed to create envelope from wizard.");
|
||||||
|
_errorMessage = ex.Message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditEnvelope()
|
void EditEnvelope()
|
||||||
@@ -932,6 +1020,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task OpenReceiverEnvelopeUrlAsync(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||||
|
{
|
||||||
|
if (!_canUseReceiverEnvelopeUrl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!CanOpenReceiverEnvelopeUrl(envelope, receiver))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var receiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlAsync(
|
||||||
|
envelope.Uuid,
|
||||||
|
receiver.Receiver!.Signature!);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(receiverEnvelopeUrl))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync("open", receiverEnvelopeUrl, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("console.warn", "[Sender] Failed to open receiver envelope URL.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool CanOpenReceiverEnvelopeUrl(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||||
|
=> !string.IsNullOrWhiteSpace(envelope.Uuid)
|
||||||
|
&& !string.IsNullOrWhiteSpace(receiver.Receiver?.Signature);
|
||||||
|
|
||||||
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
||||||
{
|
{
|
||||||
// Future: Add custom row coloring based on status if needed
|
// Future: Add custom row coloring based on status if needed
|
||||||
@@ -970,12 +1087,14 @@
|
|||||||
|
|
||||||
private async Task OpenPreviewAsync(EnvelopeDto envelope)
|
private async Task OpenPreviewAsync(EnvelopeDto envelope)
|
||||||
{
|
{
|
||||||
|
var showSigned = _activeTab == "completed" && envelope.Status == EnvelopeStatus.EnvelopeCompletelySigned;
|
||||||
|
|
||||||
// Unmount the viewer first — let the previous instance fully dispose
|
// Unmount the viewer first — let the previous instance fully dispose
|
||||||
_previewViewerMounted = false;
|
_previewViewerMounted = false;
|
||||||
_previewPdfBytes = null;
|
_previewPdfBytes = null;
|
||||||
_previewPageCount = 0;
|
_previewPageCount = 0;
|
||||||
_previewPagePreviews = [];
|
_previewPagePreviews = [];
|
||||||
_previewFileName = $"envelope-{envelope.Id}.pdf";
|
_previewFileName = showSigned ? $"envelope-{envelope.Id}-signed.pdf" : $"envelope-{envelope.Id}.pdf";
|
||||||
_previewErrorMessage = null;
|
_previewErrorMessage = null;
|
||||||
_previewLoading = true;
|
_previewLoading = true;
|
||||||
_previewVisible = true;
|
_previewVisible = true;
|
||||||
@@ -983,7 +1102,10 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_previewPdfBytes = await DocumentService.GetSenderDocumentBytesByEnvelopeIdAsync(envelope.Id);
|
_previewPdfBytes = showSigned
|
||||||
|
? await PdfSignatureService.GetSignedPdfAsync(envelope.Id)
|
||||||
|
: await DocumentService.GetSenderDocumentBytesByEnvelopeIdAsync(envelope.Id);
|
||||||
|
|
||||||
if (_previewPdfBytes is null || _previewPdfBytes.Length == 0)
|
if (_previewPdfBytes is null || _previewPdfBytes.Length == 0)
|
||||||
{
|
{
|
||||||
_previewErrorMessage = L("DocumentCouldNotBeLoaded", "Dokument konnte nicht geladen werden");
|
_previewErrorMessage = L("DocumentCouldNotBeLoaded", "Dokument konnte nicht geladen werden");
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using EnvelopeGenerator.Server.Client.Services;
|
using EnvelopeGenerator.Server.Client.Services;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
using EnvelopeGenerator.Server.Client.Options;
|
||||||
|
using MudBlazor.Services;
|
||||||
using DevExpress.Blazor.Reporting;
|
using DevExpress.Blazor.Reporting;
|
||||||
using DevExpress.XtraReports.Web.Extensions;
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
using DevExpress.DataAccess.Web;
|
using DevExpress.DataAccess.Web;
|
||||||
using DevExpress.XtraReports.Services;
|
using DevExpress.XtraReports.Services;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
|||||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||||
|
|
||||||
builder.Services.AddLocalization();
|
builder.Services.AddLocalization();
|
||||||
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
// Named HttpClient for API calls (both for services and DevExpress components)
|
// Named HttpClient for API calls (both for services and DevExpress components)
|
||||||
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
|
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
|
||||||
@@ -31,6 +34,8 @@ builder.Services.AddScoped(sp => new HttpClient
|
|||||||
// Configuration Options
|
// Configuration Options
|
||||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||||
|
builder.Services.Configure<SignatureFieldParams>(opts =>
|
||||||
|
builder.Configuration.GetSection(nameof(SignatureFieldParams)).Bind(opts));
|
||||||
|
|
||||||
// Business Services
|
// Business Services
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
@@ -45,6 +50,7 @@ builder.Services.AddScoped<EnvelopeTypeService>();
|
|||||||
builder.Services.AddScoped<LocalizationService>();
|
builder.Services.AddScoped<LocalizationService>();
|
||||||
builder.Services.AddScoped<ReceiverService>();
|
builder.Services.AddScoped<ReceiverService>();
|
||||||
builder.Services.AddScoped<PdfRenderService>();
|
builder.Services.AddScoped<PdfRenderService>();
|
||||||
|
builder.Services.AddScoped<CreateEnvelopeBootstrapStorageService>();
|
||||||
|
|
||||||
// UI Preferences & Theme
|
// UI Preferences & Theme
|
||||||
builder.Services.AddScoped<UserPreferencesService>();
|
builder.Services.AddScoped<UserPreferencesService>();
|
||||||
@@ -52,6 +58,7 @@ builder.Services.AddScoped<ThemeService>();
|
|||||||
|
|
||||||
// Logo configuration (loaded once after login, in-memory cached)
|
// Logo configuration (loaded once after login, in-memory cached)
|
||||||
builder.Services.AddScoped<CustomImagesService>();
|
builder.Services.AddScoped<CustomImagesService>();
|
||||||
|
builder.Services.AddScoped<PdfSignatureService>();
|
||||||
|
|
||||||
// DevExpress WASM
|
// DevExpress WASM
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
|
public sealed class CreateEnvelopeBootstrapStorageService(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
private const string StorageKeyPrefix = "sender.createEnvelope.bootstrap.";
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public async Task<string> StoreAsync(CreateEnvelopeBootstrapPayload payload)
|
||||||
|
{
|
||||||
|
var token = Guid.NewGuid().ToString("N");
|
||||||
|
var key = BuildStorageKey(token);
|
||||||
|
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||||
|
await jsRuntime.InvokeVoidAsync("sessionStorage.setItem", key, json);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CreateEnvelopeBootstrapPayload?> ConsumeAsync(string? token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var key = BuildStorageKey(token);
|
||||||
|
var raw = await jsRuntime.InvokeAsync<string?>("sessionStorage.getItem", key);
|
||||||
|
await jsRuntime.InvokeVoidAsync("sessionStorage.removeItem", key);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<CreateEnvelopeBootstrapPayload>(raw, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildStorageKey(string token) => $"{StorageKeyPrefix}{token}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeBootstrapPayload(
|
||||||
|
string Title,
|
||||||
|
string Message,
|
||||||
|
int? EnvelopeTypeId,
|
||||||
|
string Language,
|
||||||
|
bool UseAccessCode,
|
||||||
|
bool TFAEnabled,
|
||||||
|
CertificationType CertificationType,
|
||||||
|
int FinalEmailToCreator,
|
||||||
|
int FinalEmailToReceivers,
|
||||||
|
bool SendReminderEmails,
|
||||||
|
int FirstReminderDays,
|
||||||
|
int ReminderIntervalDays,
|
||||||
|
int ExpiresWhenDays,
|
||||||
|
int ExpiresWarningWhenDays,
|
||||||
|
IReadOnlyList<CreateEnvelopeBootstrapReceiver> Receivers,
|
||||||
|
byte[]? DocumentBytes,
|
||||||
|
string? DocumentFileName);
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeBootstrapReceiver(string FullName, string EmailAddress);
|
||||||
@@ -2,8 +2,9 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
|||||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private sealed record ReceiverEnvelopeUrlResponse(string Url);
|
||||||
|
private sealed record ReceiverEnvelopeUrlEnabledResponse(bool Enabled);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the envelope receiver data for the given envelope key from the API.
|
/// Fetches the envelope receiver data for the given envelope key from the API.
|
||||||
@@ -63,4 +66,59 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
|||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
|
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetReceiverEnvelopeUrlAsync(string envelopeUuid, string receiverSignature, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(envelopeUuid) || string.IsNullOrWhiteSpace(receiverSignature))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var url = $"/api/EnvelopeReceiver/receiver-envelope-url?envelopeUuid={Uri.EscapeDataString(envelopeUuid)}&receiverSignature={Uri.EscapeDataString(receiverSignature)}";
|
||||||
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<ReceiverEnvelopeUrlResponse>(_jsonOptions, cancel);
|
||||||
|
return payload?.Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> GetReceiverEnvelopeUrlFeatureEnabledAsync(CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var response = await http.GetAsync("/api/EnvelopeReceiver/receiver-envelope-url/enabled", cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<ReceiverEnvelopeUrlEnabledResponse>(_jsonOptions, cancel);
|
||||||
|
return payload?.Enabled == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int envelopeId, int receiverId, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
if (envelopeId <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(envelopeId));
|
||||||
|
if (receiverId <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(receiverId));
|
||||||
|
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var url = QueryHelpers.AddQueryString("/api/EnvelopeReceiver", new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["EnvelopeId"] = envelopeId.ToString(),
|
||||||
|
["ReceiverId"] = receiverId.ToString()
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await http.DeleteAsync(url, cancel);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to remove receiver. Status: {statusCode} ({reasonPhrase}){(string.IsNullOrWhiteSpace(body) ? string.Empty : $" - {body}")}",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
|||||||
string? uuid = null,
|
string? uuid = null,
|
||||||
bool? onlyActive = null,
|
bool? onlyActive = null,
|
||||||
bool? onlyCompleted = null,
|
bool? onlyCompleted = null,
|
||||||
|
bool? includeDocAndDocRes = null,
|
||||||
CancellationToken cancel = default)
|
CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var baseUrl = $"/api/Envelope";
|
var baseUrl = $"/api/Envelope";
|
||||||
@@ -46,6 +47,10 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
|||||||
{
|
{
|
||||||
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
|
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
|
||||||
}
|
}
|
||||||
|
if (includeDocAndDocRes.HasValue)
|
||||||
|
{
|
||||||
|
queryParams["IncludeDocAndDocRes"] = includeDocAndDocRes.Value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
|
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
|
public class PdfSignatureService(IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calls POST /api/PdfSignature with the envelope id and returns the fully signed PDF bytes.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> GetSignedPdfAsync(int envelopeId, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var response = await http.PostAsJsonAsync("/api/PdfSignature", new { Id = envelopeId }, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to load signed PDF. Status: {(int)response.StatusCode} ({response.ReasonPhrase ?? "Unknown"})",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
|
||||||
|
return await response.Content.ReadAsByteArrayAsync(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
@@ -26,4 +27,21 @@ public class ReceiverService(IHttpClientFactory httpClientFactory)
|
|||||||
var emails = await response.Content.ReadFromJsonAsync<List<string>>(JsonOptions, cancel);
|
var emails = await response.Content.ReadFromJsonAsync<List<string>>(JsonOptions, cancel);
|
||||||
return emails ?? [];
|
return emails ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches recent receiver suggestions for the authenticated sender.
|
||||||
|
/// Calls GET /api/EnvelopeReceiver/recent-suggestions?take={take}.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 20, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var url = $"/api/EnvelopeReceiver/recent-suggestions?take={take}";
|
||||||
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var suggestions = await response.Content.ReadFromJsonAsync<List<RecentReceiverSuggestionDto>>(JsonOptions, cancel);
|
||||||
|
return suggestions ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user