Compare commits
137 Commits
master
...
feat/servi
| Author | SHA1 | Date | |
|---|---|---|---|
| cb7d154f64 | |||
| a3f404b9ae | |||
| 1f7eb5d4ea | |||
| e1ae3ffccb | |||
| 3e3bfaa904 | |||
| f8422ed94c | |||
| c64c63925e | |||
| 2c8ae23203 | |||
| b4be718994 | |||
| 33bf5b1a51 | |||
| 6a9792bb57 | |||
| 6954a86358 | |||
| d6c5b63c49 | |||
| 8ca360d47e | |||
| 2dadefecc5 | |||
| 162f066b08 | |||
| 6592642945 | |||
| 855f22cf87 | |||
| 726673e277 | |||
| 65d615f43e | |||
| f0f92c5400 | |||
| 7e34f01f6a | |||
| f449767bf9 | |||
| f8ec6065c2 | |||
| fabfe80666 | |||
| bdb3863c07 | |||
| e5295b8302 | |||
| 00a9cf06da | |||
| 1b387238e8 | |||
| bda4f3dbef | |||
| 2458d0c07a | |||
| a72cbab195 | |||
| bcf4e63f7c | |||
| 5aabeb4510 | |||
| 32edc6474d | |||
| 71bfe3b323 | |||
| 089d2bd1cb | |||
| 65c72bcf77 | |||
| 2d8375f26a | |||
| a7cfb099fa | |||
| 7a0d4e2fa7 | |||
| 3955ee9f39 | |||
| 9bdf24d7d5 | |||
| 993ca82596 | |||
| ce9958a8b1 | |||
| 6c54473d5a | |||
| 9ad4352e02 | |||
| 90031db6a5 | |||
| bc07af9622 | |||
| 4caf8cd192 | |||
| 5423d5317b | |||
| 6c8c8f22a3 | |||
| 5100504f16 | |||
| 15f8baf54c | |||
| 4e847fa737 | |||
| 53ff48dc3c | |||
| ab67b3fba2 | |||
| 3855a8fa1e | |||
| 052da02bd0 | |||
| 53a9a3e3eb | |||
| c2ab18e184 | |||
| 8f845e8a9a | |||
| 7af934ea19 | |||
| b65367fb6d | |||
| 25c31108cb | |||
| 4083833b19 | |||
| f53bc65acf | |||
| 070d9be00c | |||
| 6f7b04a26e | |||
| 473358e2b9 | |||
| 8f3aa69cbf | |||
| eededeb1f1 | |||
| 737774f077 | |||
| 69499273cc | |||
| ead33ab2e7 | |||
| d1e2840617 | |||
| a39ef6a0e2 | |||
| 7d620988d8 | |||
| cc4a7d8c20 | |||
| 0b8068f926 | |||
| 9fd7a68798 | |||
| d6e2690bb8 | |||
| f04385a03c | |||
| c88e7b2b9e | |||
| 0ee7ec82d6 | |||
| a6d6dc8c4d | |||
| ab038df8b9 | |||
| 302249451b | |||
| a4082fca45 | |||
| f3ae8a9c49 | |||
| d6058c41d0 | |||
| 79d093c492 | |||
| 56c65b6fbb | |||
| 2af18842c4 | |||
| 41e0d4691b | |||
| 64e0a4f749 | |||
| 4cf54d36b9 | |||
| 020cecabf3 | |||
| 40c899e47e | |||
| bae62c7c08 | |||
| 41dde6f016 | |||
| c5b167f0d4 | |||
| a70faebde6 | |||
| 3e01052579 | |||
| f49b907574 | |||
| 86ed96ae76 | |||
| 9dbfdaa15e | |||
| 9d66f1d19e | |||
| 14cef05d02 | |||
| bdfb973d55 | |||
| 15a18b1bfd | |||
| 6fac1cd96a | |||
| 79d2636c14 | |||
| 2172ce8203 | |||
| 51ab9fb094 | |||
| d8a002cd22 | |||
| e36684820e | |||
| 2a5d953623 | |||
| 0aba9e91e2 | |||
| 1a0973075b | |||
| b8fd26611c | |||
| 0ca372bf45 | |||
| 5230076d5d | |||
| b28084bf19 | |||
| cbc983e070 | |||
| 3b06f3fdac | |||
| a12d74871d | |||
| 45b715ed74 | |||
| 9d5e2e6ad2 | |||
| c5d2d79563 | |||
| 15d4573321 | |||
| eb46590c1d | |||
| c93c32307a | |||
| 41cca7fa64 | |||
| b01c17ab18 | |||
| c8834dc3be | |||
| e385fdda95 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -365,9 +365,3 @@ FodyWeavers.xsd
|
||||
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
||||
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
||||
/EnvelopeGenerator.Tests.Application/annotations.json
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||
/ignore/EnvelopeGenerator.Form
|
||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json
|
||||
|
||||
415
AGENTS.md
415
AGENTS.md
@@ -1,415 +0,0 @@
|
||||
# EnvelopeGenerator - Agent Guide
|
||||
|
||||
## Must Read First
|
||||
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
||||
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
|
||||
|
||||
---
|
||||
|
||||
## Documentation Catalog
|
||||
|
||||
This section explains every markdown file in the repo: what it contains, why it exists, and when you need it. Read this before deciding which file to open.
|
||||
|
||||
### Core Architecture Docs (Active — Always Relevant)
|
||||
|
||||
#### `AGENTS.md` ← you are here
|
||||
**What:** Authoritative agent/copilot onboarding guide. Covers project structure, route table, render mode rules, coordinate system, API gaps, and common mistakes.
|
||||
**Why it exists:** Single source of truth for agents so they don't re-derive architecture from code on every session.
|
||||
**When you need it:** Always. Read first on every session.
|
||||
**Maintenance:** Update whenever project structure, routes, render modes, or naming conventions change.
|
||||
|
||||
#### `COPILOT_CONTEXT.md`
|
||||
**What:** Deep-dive architecture reference. Covers the full Blazor Auto hybrid setup, authentication flows (JWT + cookies, per-envelope receiver tokens), service registration patterns, coordinate system proof, and an explicit "prefer Server/Server.Client over old WebUI/ReceiverUI" rule.
|
||||
**Why it exists:** Prevents agents from re-deriving or misinterpreting the hybrid SSR+WASM architecture. Also documents coordinate system evidence to prevent regressions (inches vs. points vs. pixels).
|
||||
**When you need it:**
|
||||
- Implementing any new page or service
|
||||
- Debugging auth/cookie issues
|
||||
- Any coordinate conversion question
|
||||
- Unsure which project a new file should go in
|
||||
**Maintenance:** Update when render mode assignments change, new auth schemes are added, or the coordinate system is touched.
|
||||
|
||||
#### `FORM_APPLICATION_CONTEXT.md`
|
||||
**What:** Comprehensive reverse-engineering of the legacy VB.NET WinForms app (`EnvelopeGenerator.Form`). Documents every form (frmMain, frmEnvelopeEditor, frmFieldEditor, etc.), their UI layout, toolbar actions, data models, coordinate system, and a feature-by-feature migration mapping table to `EnvelopeGenerator.Server`.
|
||||
**Why it exists:** The WinForms app is the functional specification for the sender-side web UI. It documents the full intended workflow that is being migrated to Blazor — especially the envelope creation, signature field placement, and dashboard features that are not yet implemented.
|
||||
**When you need it:**
|
||||
- Implementing any sender-side feature (`/sender`, `/sender/editor`, `/sender/envelope/{id}`)
|
||||
- Understanding what a specific toolbar button or form should do
|
||||
- Designing API endpoints for sender workflow
|
||||
- Checking coordinate system behavior (INCHES proof is here)
|
||||
**Maintenance:** Update the mapping table as features are implemented (change `❌ Not implemented` → `✅ Exists`). Do not rename `ReceiverUI` back — it was the old project name; current target is `EnvelopeGenerator.Server`.
|
||||
|
||||
#### `RECEIVER_PDF_VIEWER_CONTEXT.md`
|
||||
**What:** Detailed technical reference for the receiver-side PDF viewer (`EnvelopeReceiverPage.razor`). Documents the PDF.js integration, signature button rendering, JS interop API (`window.pdfViewer`), signature capture flow, HiDPI/zoom/thumbnail config, and the full component lifecycle.
|
||||
**Why it exists:** The receiver PDF viewer is the most complex component in the codebase. This doc prevents agents from misunderstanding the JS↔Blazor boundary, re-implementing already-solved problems (HiDPI, zoom, signature caching), or breaking the signing flow.
|
||||
**When you need it:**
|
||||
- Working on `EnvelopeReceiverPage.razor` or `pdf-viewer.js`
|
||||
- Adding new signature field rendering logic
|
||||
- Debugging signature rendering, zoom, or thumbnail issues
|
||||
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
|
||||
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
|
||||
|
||||
#### `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
|
||||
|
||||
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
||||
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
|
||||
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
|
||||
**When you need it:**
|
||||
- Deploying to IIS for the first time
|
||||
- Troubleshooting 500 errors on production after deploy
|
||||
- Setting up a new deployment environment
|
||||
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
|
||||
|
||||
---
|
||||
|
||||
## Markdown Audit Snapshot (2026-10-05)
|
||||
|
||||
The following table summarizes repository markdown files by practical status: completed, partially complete, or removable candidates.
|
||||
|
||||
| File | Type | Status | Missing / Gap | Removal Recommendation |
|
||||
|---|---|---|---|---|
|
||||
| `AGENTS.md` | Agent onboarding | Partially complete | Route/migration statements can drift from real code over time. | Keep |
|
||||
| `COPILOT_CONTEXT.md` | Architecture reference | Partially complete | Some route/render details may be out of sync with other docs; needs regular reconciliation. | Keep |
|
||||
| `FORM_APPLICATION_CONTEXT.md` | Legacy-to-modern feature spec | Partially complete | Large sections still marked not implemented / needs implementation. | Keep |
|
||||
| `RECEIVER_PDF_VIEWER_CONTEXT.md` | Receiver PDF viewer deep-dive | Partially complete | Long migration plan needs periodic sync with active implementation. | Keep |
|
||||
| `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` | Sender workflow implementation plan | Partially complete | No explicit implementation progress markers per phase. | Keep (until migration closes) |
|
||||
| `SENDER_PDF_TOOLBAR_PLAN_EN.md` | Sender PDF toolbar plan | Incomplete | Explicit pending item remains (`Toolbar visual polish`). | Keep (until done) |
|
||||
| `SENDER_EDITOR_WASM_PDF_EDITOR_IMPLEMENTATION_PLAN_en.md` | Sender editor WASM migration plan | Incomplete | Plan exists but completion/progress tracking is minimal. | Keep (until done) |
|
||||
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md` | Deployment guide | Complete (operational) | No critical gap identified. | Keep |
|
||||
| `envelope-generator-ui/README.md` | Angular scaffold README | Complete but generic | Template text only; low project-specific value. | Removable or replace with project-specific summary |
|
||||
| `EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Third-party vendor README | Complete (vendor) | Not project workflow documentation. | Keep with vendor assets |
|
||||
| `EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Third-party license | Complete (legal) | No gap. | Keep |
|
||||
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md` | Third-party vendor README | Complete (vendor) | Not project-specific guidance. | Keep with vendor assets |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/README.md` | Legacy PSPDFKit note | Outdated | Mentions deprecated PSPDFKit-based flow; may mislead if reused. | Removable/archive |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Duplicate vendor README | Redundant | Duplicate copy under `ignore/`. | Removable/archive |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/bootstrap-icons/README.md` | Vendor README in archive area | Secondary | Archive-only utility, not active docs. | Removable/archive |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Duplicate license in archive area | Redundant | Duplicate copy under `ignore/`. | Removable/archive (if legal policy allows) |
|
||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/README.md` | Duplicate Angular scaffold README | Redundant | Generic template + duplicate in archive path. | Removable/archive |
|
||||
|
||||
### Documentation Audit TODO
|
||||
|
||||
- [ ] Validate this markdown audit table against the current codebase and update statuses (`complete`, `partial`, `incomplete`, `removable`) when implementation changes land.
|
||||
|
||||
---
|
||||
|
||||
## Active Architecture (Post-Migration)
|
||||
|
||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||
|
||||
**Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
|
||||
|
||||
**Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
|
||||
|
||||
### Deprecated Projects - DO NOT USE
|
||||
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
||||
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
|
||||
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Run Active Host
|
||||
```powershell
|
||||
# Terminal - Blazor + Web API host
|
||||
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||
dotnet run
|
||||
```
|
||||
|
||||
**Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
|
||||
|
||||
### Build
|
||||
```powershell
|
||||
dotnet build EnvelopeGenerator.sln
|
||||
```
|
||||
|
||||
## Project Boundaries
|
||||
|
||||
```
|
||||
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
|
||||
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
||||
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
||||
EnvelopeGenerator.Server/
|
||||
EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
|
||||
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||
├─ Controllers/ # Active Web API endpoints
|
||||
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
||||
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||
├─ Services/ # HTTP API clients
|
||||
├─ Models/ # DTOs
|
||||
```
|
||||
|
||||
## Route Structure (Critical)
|
||||
|
||||
| Route | File Location | Render Mode | Purpose |
|
||||
|-------|--------------|-------------|---------|
|
||||
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
|
||||
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
||||
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
|
||||
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
|
||||
| `/envelope/login/{key}` | `Server.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
||||
| `/envelope/{key}` | `Server/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
|
||||
|
||||
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
|
||||
|
||||
## Coordinate System (CRITICAL)
|
||||
|
||||
**Database stores INCHES** (GdPicture14 native). Origin: top-left, Y-axis down.
|
||||
|
||||
### Conversions
|
||||
```csharp
|
||||
// Database (INCHES) → PDF Points
|
||||
float points = inches * 72;
|
||||
|
||||
// Database (INCHES) → DevExpress DX
|
||||
float dx = inches * 100;
|
||||
|
||||
// PDF.js Pixels → Database (INCHES)
|
||||
float inches = (pixelX / canvasWidth) * pageWidthInches;
|
||||
```
|
||||
|
||||
**A4 Page:** 8.27" wide × 11.69" tall = 595pt × 842pt
|
||||
|
||||
**Signature Field Size:** 1.77" × 1.96" (FIXED, do not change)
|
||||
|
||||
**Evidence:** See `COPILOT_CONTEXT.md` lines 158-185, `EnvelopeGenerator.Form/frmFieldEditor.vb`
|
||||
|
||||
## API Architecture Quirks
|
||||
|
||||
### Monolithic Endpoint (Avoid for UI)
|
||||
`POST /api/EnvelopeReceiver` - Creates envelope+document+receivers+fields atomically.
|
||||
- **Use case:** External API consumers
|
||||
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
|
||||
|
||||
### Sender Workflow Endpoint
|
||||
```
|
||||
POST /api/Envelope # Create/update/save/send envelope workflow payload
|
||||
DELETE /api/Envelope/{id} # Delete with reason
|
||||
POST /api/Envelope/{id}/document # Upload PDF
|
||||
POST /api/Envelope/{id}/receivers # Add receiver
|
||||
POST /api/Envelope/{id}/signature-fields # Place signature field
|
||||
```
|
||||
|
||||
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
||||
|
||||
## Status Color Coding
|
||||
|
||||
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
|
||||
|
||||
```css
|
||||
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
||||
.envelope-row.status-queued,
|
||||
.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */
|
||||
.envelope-row.status-completed { background-color: #81C784; }
|
||||
.envelope-row.status-deleted,
|
||||
.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
|
||||
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
||||
|
||||
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
|
||||
```json
|
||||
{
|
||||
"PdfViewerOptions": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"ZoomStepPercentage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
|
||||
- `ConnectionStrings:Default` - SQL Server DB
|
||||
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
||||
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
||||
- `PSPDFKitLicenseKey` - **DEPRECATED** (use PDF.js instead)
|
||||
|
||||
## Migration Status
|
||||
|
||||
### Complete ✅
|
||||
- Receiver login/authentication
|
||||
- PDF viewing with PDF.js (HiDPI, zoom, thumbnails)
|
||||
- Signature capture (draw/type/image)
|
||||
- Signature caching (Redis/SQL)
|
||||
- Sender login
|
||||
|
||||
### Missing (High Priority) ❌
|
||||
- Sender dashboard (`/sender`) - Empty stub
|
||||
- Envelope editor (`/sender/envelope/{id}`)
|
||||
- Signature field placement tool (PDF.js + draggable overlays)
|
||||
- Granular API endpoints (draft, receivers, fields)
|
||||
- Master-detail grids for receivers/history
|
||||
|
||||
## Common Mistakes (DO NOT REPEAT)
|
||||
|
||||
| Mistake | Why Wrong |
|
||||
|---------|-----------|
|
||||
| Using iText7 in receiver pages | GPL license issue. Use PDF.js overlays. |
|
||||
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
||||
| `@rendermode InteractiveWebAssembly` on PDF viewers | DevExpress DxPdfViewer requires server-side rendering. |
|
||||
| Hardcoded quality in PDF.js | Use `appsettings.json` `PdfViewerOptions`. |
|
||||
| Coordinates in points/pixels for DB | Database uses INCHES. Convert before save. |
|
||||
| `BottomMarginBand` for signatures | Repeats on every page. Use `DetailBand`. |
|
||||
|
||||
## Testing
|
||||
|
||||
**No automated tests exist yet.**
|
||||
|
||||
Manual testing workflow:
|
||||
1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||
2. Navigate to `https://localhost:5131` (or check console output for port)
|
||||
3. Test sender login at `/sender/login`
|
||||
4. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||
|
||||
## Technical Debt TODOs
|
||||
|
||||
- **CRITICAL (Licensing / pre-commercial release):** Remove all `SixLabors.ImageSharp` and `QRCoder-ImageSharp` package dependencies from production/runtime projects before commercial release. Replace remaining QR/image paths with permissive-license alternatives (no copyleft/share-source obligations), run repo-wide package audit (`*.csproj`), and add CI guardrails to block reintroduction.
|
||||
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
|
||||
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
|
||||
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
|
||||
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
|
||||
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
|
||||
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
|
||||
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
|
||||
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
|
||||
- Move sender PDF image editor initial zoom from hardcoded value to Options pattern configuration (bind from `appsettings.json` and inject via `IOptions`/`IOptionsMonitor`) so default zoom (for example `%180`) is environment-configurable.
|
||||
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
|
||||
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
||||
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
||||
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
||||
- Implement SPA-style language switching so the UI culture updates immediately without a full page reload.
|
||||
- Implement sender-scoped caching for "recent receiver suggestions" used during envelope creation (wizard/editor flows): cache key should include sender identity, support short TTL + invalidation hooks after successful envelope receiver writes, and prevent cross-sender suggestion leakage.
|
||||
|
||||
## Database
|
||||
|
||||
**SQL Server** (DD_ECM)
|
||||
- Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
|
||||
- EF Core migrations NOT used (manual SQL scripts)
|
||||
- Stored procedures: `PRSIG_*` prefix
|
||||
|
||||
**Key Tables:**
|
||||
- `TBSIG_ENVELOPE` - Envelope metadata
|
||||
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
|
||||
- `TBSIG_DOCUMENT_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
|
||||
- `TBSIG_RECEIVER` - Receiver registry
|
||||
- `TBSIG_ENVELOPE_DOCUMENT` - PDF binary data
|
||||
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
||||
|
||||
### Data Structure (Domain + EGDbContext)
|
||||
|
||||
Use this as the canonical quick map before touching sender/receiver workflow code.
|
||||
|
||||
#### Core Aggregate Shape
|
||||
- `Envelope` (`TBSIG_ENVELOPE`) is the root aggregate for sending workflow.
|
||||
- `Envelope -> Documents` is `1:N` via `Document.EnvelopeId` (`TBSIG_ENVELOPE_DOCUMENT`).
|
||||
- `Envelope -> Histories` is `1:N` via `History.EnvelopeId` (`TBSIG_ENVELOPE_HISTORY`).
|
||||
- `Envelope <-> Receiver` is `N:N` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{EnvelopeId, ReceiverId}`).
|
||||
- `Document -> DocReceiverElement` is `1:N` via `DocReceiverElement.DocumentId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT`).
|
||||
- `DocReceiverElement -> ElementAnnotation` is `1:N` via `ElementAnnotation.ElementId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`).
|
||||
|
||||
#### Status and Tracking Entities
|
||||
- `DocumentStatus` (`TBSIG_DOCUMENT_STATUS`) links `EnvelopeId + ReceiverId` and stores receiver-level signing state.
|
||||
- `History` stores timeline events (`EnvelopeStatus`) with polymorphic `UserReference` lookup semantics.
|
||||
- `EnvelopeReceiverReadOnly` (`TBSIG_ENVELOPE_RECEIVER_READ_ONLY`) stores temporary read-only delegation with validity window (`DateValid`).
|
||||
|
||||
#### Important Constraints From `EGDbContext`
|
||||
- `Envelope.Uuid` has a unique index.
|
||||
- `Receiver.Signature` has a unique index.
|
||||
- `Receiver.EmailAddress` has a unique index.
|
||||
- `EnvelopeReceiver` uses composite primary key: `{ EnvelopeId, ReceiverId }`.
|
||||
- `History.UserReference` is mapped conditionally (`IsMigration == false`) to:
|
||||
- `Receiver.EmailAddress` (`History.Receiver`)
|
||||
- `User.Email` from UserManager (`History.Sender`)
|
||||
- `EnvelopeReceiverReadOnly.AddedWho` maps to `Receiver.EmailAddress` (principal key mapping, not receiver GUID).
|
||||
|
||||
#### Coordinate-Critical Fields
|
||||
- Signature placement is persisted in `DocReceiverElement` as `X`, `Y`, `Width`, `Height`, `Page`.
|
||||
- These coordinates are in INCHES (not PDF points, not pixels).
|
||||
|
||||
#### Cross-Module Tables in Same Context
|
||||
- `EGDbContext` also includes UserManager entities: `User`, `Group`, `Module`, `GroupOfUser`, `ModuleOfUser`, `UserRep`, and `ClientUser`.
|
||||
- Mail dispatcher entity `EmailOut` is also registered under `#if NET` and trigger-configurable.
|
||||
|
||||
#### Trigger-Aware Model Notes
|
||||
- Trigger names are injected from `DbTriggerParams` in appsettings and applied via `ToTable(tb => tb.HasTrigger(...))`.
|
||||
- Current trigger registration is explicit (`AddTrigger<T>()` calls), not attribute/reflection driven.
|
||||
- Do not remove trigger mappings casually; this project depends on DB-side automation/audit behavior.
|
||||
|
||||
## DevExpress
|
||||
|
||||
**License:** Commercial (v25.2.3)
|
||||
**Components Used:**
|
||||
- `DxGrid` - Master-detail grids
|
||||
- `DxPdfViewer` - Server-side PDF rendering
|
||||
- `DxPopup` - Modal dialogs
|
||||
- `DxToolbar` - Action bars
|
||||
- `DxFormLayout` - Forms
|
||||
|
||||
**Theme:** Blazing Berry (default)
|
||||
|
||||
## JavaScript Interop
|
||||
|
||||
**PDF Viewer:** `wwwroot/js/pdf-viewer.js`
|
||||
```javascript
|
||||
window.pdfViewer = {
|
||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
||||
renderPage(num),
|
||||
renderSignatureButtons(signatures, pageNum, dotNetRef),
|
||||
applySignature(signatureId, dataUrl, fullName, position, place),
|
||||
zoomIn(), zoomOut(), dispose()
|
||||
}
|
||||
```
|
||||
|
||||
**Signature Pad:** `wwwroot/js/receiver-signature.js`
|
||||
```javascript
|
||||
window.receiverSignature = {
|
||||
initializeDrawPad(canvasId, dotNetRef),
|
||||
getSignatureDataUrl(canvasId),
|
||||
clearPad(canvasId)
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Envelope Support
|
||||
|
||||
Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies:
|
||||
```
|
||||
AuthTokenSignFLOWReceiver.{envelopeKey}
|
||||
```
|
||||
|
||||
Each envelope maintains independent authentication state.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
**CDN:**
|
||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||
|
||||
**NuGet (Server.Client):**
|
||||
- `DevExpress.Blazor.*` 25.2.3
|
||||
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
||||
|
||||
**External Services:**
|
||||
- LDAP/AD authentication (optional)
|
||||
- GTX Messaging (SMS 2FA)
|
||||
- Email dispatcher (signFlow)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
None required. All config in `appsettings.json`.
|
||||
|
||||
**Local dev ports:**
|
||||
- App host (UI + API): `https://localhost:5131` or `http://localhost:7192`
|
||||
@@ -1,351 +0,0 @@
|
||||
# EnvelopeGenerator — Current Workspace Context
|
||||
|
||||
## Purpose
|
||||
Digital document signing system for senders and receivers.
|
||||
|
||||
- Senders authenticate, view envelope lists, and manage envelope workflows.
|
||||
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
|
||||
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
|
||||
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
|
||||
|
||||
---
|
||||
|
||||
## Active Application Structure
|
||||
|
||||
### Main Host
|
||||
**Primary active application:** `EnvelopeGenerator.Server`
|
||||
|
||||
`EnvelopeGenerator.Server` is the current runtime host and contains:
|
||||
- Blazor server host
|
||||
- WebAssembly host integration
|
||||
- API controllers
|
||||
- authentication/authorization setup
|
||||
- Swagger/Scalar setup
|
||||
- YARP reverse proxy configuration
|
||||
- DevExpress server-side services
|
||||
- SQL Server distributed cache setup
|
||||
|
||||
### Client Project
|
||||
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||
|
||||
This project contains:
|
||||
- WebAssembly-rendered pages
|
||||
- client-side services
|
||||
- client models and options
|
||||
- sender and receiver login flows
|
||||
|
||||
### Other Projects
|
||||
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
|
||||
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||
- `EnvelopeGenerator.API` — deprecated project (no longer part of the active solution/runtime path)
|
||||
|
||||
### Legacy / Do Not Touch
|
||||
- `EnvelopeGenerator.Service`
|
||||
- `EnvelopeGenerator.Form`
|
||||
- `EnvelopeGenerator.BBTests`
|
||||
- `EnvelopeGenerator.CommonServices`
|
||||
|
||||
---
|
||||
|
||||
## Current Hosting Model
|
||||
|
||||
`EnvelopeGenerator.Server/Program.cs` currently configures:
|
||||
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
|
||||
- `AddControllers()` and `MapControllers()`
|
||||
- JWT authentication for sender and receiver flows
|
||||
- cookie authentication
|
||||
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
|
||||
- `AddReverseProxy()` with `yarp.json`
|
||||
- Swagger / OpenAPI / Scalar
|
||||
- distributed SQL Server cache
|
||||
- DevExpress Blazor and DevExpress PDF Viewer server-side services
|
||||
- request localization middleware
|
||||
|
||||
This means the active app is a **merged UI + API host**.
|
||||
|
||||
---
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||
|
||||
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
|
||||
|
||||
Configured routes forward:
|
||||
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
|
||||
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
|
||||
|
||||
---
|
||||
|
||||
## Active Routes and Files
|
||||
|
||||
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||
| Route | File | Render Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
|
||||
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
|
||||
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
|
||||
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
|
||||
|
||||
### Server Pages (`EnvelopeGenerator.Server`)
|
||||
| Route | File | Render Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
|
||||
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
|
||||
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
|
||||
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
|
||||
|
||||
---
|
||||
|
||||
## Current API Location
|
||||
|
||||
The active application exposes controllers from:
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||
|
||||
Current controller set includes:
|
||||
- `AnnotationController`
|
||||
- `AuthController`
|
||||
- `CacheController`
|
||||
- `ConfigController`
|
||||
- `DocumentController`
|
||||
- `EmailTemplateController`
|
||||
- `EnvelopeController`
|
||||
- `EnvelopeReceiverController`
|
||||
- `EnvelopeTypeController`
|
||||
- `HistoryController`
|
||||
- `LocalizationController`
|
||||
- `ReadOnlyController`
|
||||
- `ReceiverController`
|
||||
- `SignatureController`
|
||||
- `TfaRegistrationController`
|
||||
|
||||
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Model
|
||||
|
||||
### Sender
|
||||
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||
|
||||
Key sender endpoints:
|
||||
- `POST /api/auth?cookie=true` — login
|
||||
- `GET /api/auth/check` — current sender access check
|
||||
- `POST /api/auth/logout` — logout
|
||||
|
||||
### Receiver
|
||||
Receiver authentication is **per envelope**.
|
||||
|
||||
Key receiver endpoints used by client services:
|
||||
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
|
||||
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
|
||||
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
|
||||
|
||||
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||
|
||||
### Receiver Server-Side Authorization
|
||||
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||
|
||||
It uses:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||
|
||||
Behavior:
|
||||
- tries the current `HttpContext.User`
|
||||
- if needed, reads the per-envelope receiver cookie directly
|
||||
- validates the JWT with the receiver auth scheme
|
||||
- verifies the token subject matches the route envelope key
|
||||
|
||||
---
|
||||
|
||||
## Receiver Page Data Loading
|
||||
|
||||
Main server-side page data service:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||
|
||||
This service loads directly via MediatR and distributed cache:
|
||||
- document bytes
|
||||
- receiver envelope data
|
||||
- signature placeholders
|
||||
- cached signature data
|
||||
|
||||
For signature placeholders, the service:
|
||||
- reads document receiver elements
|
||||
- filters them for the authenticated receiver
|
||||
- converts coordinates to `UnitOfLength.Point` before UI use
|
||||
|
||||
---
|
||||
|
||||
## Receiver PDF Viewer
|
||||
|
||||
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
|
||||
Current receiver viewer characteristics:
|
||||
- route: `/envelope/{EnvelopeKey}`
|
||||
- render mode: `InteractiveServer`
|
||||
- PDF rendering: `PDF.js`
|
||||
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
|
||||
- signature popup: `DxPopup`
|
||||
- thumbnail sidebar: resizable and stored in `localStorage`
|
||||
|
||||
### JS Assets
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||
|
||||
### CSS
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
|
||||
|
||||
### PDF.js CDN
|
||||
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
|
||||
|
||||
---
|
||||
|
||||
## Signature Workflow
|
||||
|
||||
Receiver signatures are handled as a **viewer overlay workflow**.
|
||||
|
||||
### Current behavior
|
||||
1. Server-side authorization validates receiver access.
|
||||
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
|
||||
3. If no cached signature exists, the signature popup opens automatically.
|
||||
4. Receiver creates signature using one of three tabs:
|
||||
- draw
|
||||
- text
|
||||
- image
|
||||
5. Required metadata:
|
||||
- full name
|
||||
- place
|
||||
6. Optional metadata:
|
||||
- position
|
||||
7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer.
|
||||
|
||||
### Important note
|
||||
Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer.
|
||||
|
||||
### Signature DTO
|
||||
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
|
||||
|
||||
```csharp
|
||||
public sealed record SignatureCaptureDto {
|
||||
public required string DataUrl { get; init; }
|
||||
public required string FullName { get; init; }
|
||||
public string Position { get; init; } = "";
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signature Cache
|
||||
|
||||
### Active cache model
|
||||
The current receiver page cache flow is handled directly in the server project through:
|
||||
- `EnvelopeReceiverPageDataService`
|
||||
- `IDistributedCache`
|
||||
- SQL Server distributed cache configuration from `Program.cs`
|
||||
|
||||
### Cache key format
|
||||
Current server-side key prefix:
|
||||
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||
|
||||
This is different from an envelope-key-only cache convention.
|
||||
|
||||
### Config
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||
- section name: `Cache`
|
||||
- option: `SignatureCacheExpiration`
|
||||
|
||||
### Related controller
|
||||
A cache API controller also exists in:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||
|
||||
---
|
||||
|
||||
## Sender Dashboard
|
||||
|
||||
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||
|
||||
Current behavior:
|
||||
- checks sender access through `AuthService.CheckSenderAccessAsync()`
|
||||
- redirects to `/sender/login` when unauthorized
|
||||
- loads envelope list through client `EnvelopeService`
|
||||
- separates envelopes into active/completed tabs
|
||||
- uses `DevExpress DxGrid`
|
||||
|
||||
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||
|
||||
---
|
||||
|
||||
## Localization
|
||||
|
||||
Current server host localization setup in `Program.cs`:
|
||||
- supported cultures: `de-DE`, `en-US`
|
||||
- request localization middleware is enabled
|
||||
- `QueryStringRequestCultureProvider` is added
|
||||
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||
|
||||
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
### Source data
|
||||
Database signature coordinates are still based on:
|
||||
- **unit:** inches
|
||||
- **origin:** top-left
|
||||
- **axes:** X right, Y down
|
||||
|
||||
### Relevant conversions
|
||||
- inches -> PDF points: `x_pt = x_inches * 72`
|
||||
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
|
||||
|
||||
### Current receiver page behavior
|
||||
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
|
||||
|
||||
### Unit systems to keep in mind
|
||||
| System | Unit | Origin | Y-axis |
|
||||
|---|---|---|---|
|
||||
| Database | Inches | Top-left | Down |
|
||||
| PDF.js display | Pixels | Top-left | Down |
|
||||
| PDF points | Points | Depends on PDF model | Depends on consumer |
|
||||
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
|
||||
|
||||
---
|
||||
|
||||
## Key Services and Files
|
||||
|
||||
### Client services
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||
|
||||
### Server services
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||
|
||||
### Server config and host files
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||
|
||||
---
|
||||
|
||||
## Working Rules for This Workspace
|
||||
|
||||
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
|
||||
- Do not modify the legacy VB.NET projects unless explicitly requested.
|
||||
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
|
||||
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-06-29
|
||||
@@ -1,384 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,21 +1,19 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages annotations and signature lifecycle for envelopes.
|
||||
@@ -61,8 +59,8 @@ public class AnnotationController : ControllerBase
|
||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||
{
|
||||
var signature = User.ReceiverSignature();
|
||||
var uuid = User.EnvelopeUuid();
|
||||
var signature = User.GetReceiverSignatureOfReceiver();
|
||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||
|
||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
||||
|
||||
@@ -74,24 +72,12 @@ public class AnnotationController : ControllerBase
|
||||
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||
|
||||
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
||||
var docSignedNotification = envelopeReceiverDto is not null
|
||||
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
||||
: throw new NotFoundException("Envelope receiver is not found.");
|
||||
var docSignedNotification = await _mediator
|
||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
|
||||
try
|
||||
{
|
||||
await _mediator.Publish(docSignedNotification, cancel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _mediator.Publish(new RemoveSignatureNotification()
|
||||
{
|
||||
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
|
||||
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
|
||||
}, cancel);
|
||||
throw;
|
||||
}
|
||||
await _mediator.PublishSafely(docSignedNotification, cancel);
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
return Ok();
|
||||
@@ -100,16 +86,15 @@ public class AnnotationController : ControllerBase
|
||||
/// <summary>
|
||||
/// Rejects the document for the current receiver.
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey">Envelope key route parameter.</param>
|
||||
/// <param name="reason">Optional rejection reason.</param>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("reject/{envelopeKey}")]
|
||||
[HttpPost("reject")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> Reject([FromRoute] string envelopeKey, [FromBody] string? reason = null)
|
||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||
{
|
||||
var signature = User.ReceiverSignature();
|
||||
var uuid = User.EnvelopeUuid();
|
||||
var mail = User.ReceiverMail();
|
||||
var signature = User.GetReceiverSignatureOfReceiver();
|
||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||
var mail = User.GetReceiverMailOfReceiver();
|
||||
|
||||
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||
|
||||
76
EnvelopeGenerator.API/Controllers/AuthController.cs
Normal file
76
EnvelopeGenerator.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using EnvelopeGenerator.API.Controllers.Interfaces;
|
||||
using EnvelopeGenerator.API.Models;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus.
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions, IAuthorizationService authService) : ControllerBase, IAuthController
|
||||
{
|
||||
private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IAuthorizationService AuthService { get; } = authService;
|
||||
|
||||
/// <summary>
|
||||
/// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie)
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Gibt eine HTTP 200 oder 401.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/auth/logout
|
||||
///
|
||||
/// </remarks>
|
||||
/// <response code="200">Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat.</response>
|
||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
||||
Response.Cookies.Delete(authTokenKeys.Cookie);
|
||||
else if (await this.IsUserInPolicyAsync(AuthPolicy.ReceiverOrReceiverTFA))
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
else
|
||||
return Unauthorized();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prüft, ob der Benutzer ein autorisiertes Token hat.
|
||||
/// </summary>
|
||||
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// GET /api/auth
|
||||
///
|
||||
/// </remarks>
|
||||
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
|
||||
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[HttpGet("check")]
|
||||
[Authorize]
|
||||
public IActionResult Check(string? role = null)
|
||||
=> role is not null && !User.IsInRole(role)
|
||||
? Unauthorized()
|
||||
: Ok();
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes configuration data required by the client applications.
|
||||
63
EnvelopeGenerator.API/Controllers/DocumentController.cs
Normal file
63
EnvelopeGenerator.API/Controllers/DocumentController.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using EnvelopeGenerator.API.Controllers.Interfaces;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to envelope documents for authenticated receivers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="DocumentController"/> class.
|
||||
/// </remarks>
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DocumentController(IMediator mediator, IAuthorizationService authService) : ControllerBase, IAuthController
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IAuthorizationService AuthService => authService;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the document bytes receiver.
|
||||
/// </summary>
|
||||
/// <param name="query">Encoded envelope key.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
|
||||
{
|
||||
// Sender: expects query with envelope key
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
||||
{
|
||||
if (query is null)
|
||||
return BadRequest("Missing document query.");
|
||||
|
||||
var senderDoc = await mediator.Send(query, cancel);
|
||||
return senderDoc.ByteData is byte[] senderDocByte
|
||||
? File(senderDocByte, "application/octet-stream")
|
||||
: NotFound("Document is empty.");
|
||||
}
|
||||
|
||||
// Receiver: resolve envelope id from claims
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Receiver))
|
||||
{
|
||||
if (query is not null)
|
||||
return BadRequest("Query parameters are not allowed for receiver role.");
|
||||
|
||||
var envelopeId = User.GetEnvelopeIdOfReceiver();
|
||||
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
|
||||
return receiverDoc.ByteData is byte[] receiverDocByte
|
||||
? File(receiverDocByte, "application/octet-stream")
|
||||
: NotFound("Document is empty.");
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.EmailTemplates.Queries;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing temp templates.
|
||||
@@ -24,7 +24,7 @@ namespace EnvelopeGenerator.Server.Controllers;
|
||||
/// </param>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[Authorize(Policy = AuthPolicy.Sender)]
|
||||
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -1,13 +1,12 @@
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using EnvelopeGenerator.Application.PdfSigning;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
|
||||
@@ -23,16 +22,25 @@ namespace EnvelopeGenerator.Server.Controllers;
|
||||
/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden.
|
||||
/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
||||
/// </remarks>
|
||||
/// <param name="Logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
||||
/// <param name="Mediator"></param>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class EnvelopeController(ILogger<EnvelopeController> Logger, IMediator Mediator) : ControllerBase
|
||||
public class EnvelopeController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EnvelopeController> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
||||
/// </summary>
|
||||
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
||||
/// <param name="mediator"></param>
|
||||
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
|
||||
/// </summary>
|
||||
@@ -43,12 +51,12 @@ public class EnvelopeController(ILogger<EnvelopeController> Logger, IMediator Me
|
||||
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
||||
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||
{
|
||||
var result = await Mediator.Send(envelope.Authorize(User.GetId()));
|
||||
return Ok(result ?? []);
|
||||
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
|
||||
return result.Any() ? Ok(result) : NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +71,8 @@ public class EnvelopeController(ILogger<EnvelopeController> Logger, IMediator Me
|
||||
[HttpGet("doc-result")]
|
||||
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
||||
{
|
||||
var envelopes = await Mediator.Send(query.Authorize(User.GetId()));
|
||||
query.IncludeDocResult = true;
|
||||
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
|
||||
var envelope = envelopes.FirstOrDefault();
|
||||
|
||||
if (envelope is null)
|
||||
@@ -85,57 +94,19 @@ public class EnvelopeController(ILogger<EnvelopeController> Logger, IMediator Me
|
||||
/// </summary>
|
||||
/// <param name="command"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[NonAction]
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||
{
|
||||
var res = await Mediator.Send(command.WithAuth(User.GetId()));
|
||||
var res = await _mediator.Send(command.Authorize(User.GetId()));
|
||||
|
||||
if (res is null)
|
||||
{
|
||||
Logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
||||
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
else
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Reason"></param>
|
||||
public record DeleteEnvelopeRequest(string Reason);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||
{
|
||||
var result = await Mediator.Send(new DeleteEnvelopeCommand
|
||||
{
|
||||
EnvelopeId = id,
|
||||
Reason = request?.Reason ?? string.Empty
|
||||
}.WithAuth(User.GetId()));
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="command"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpPost("finalize")]
|
||||
public async Task<IActionResult> FinalizeEnvelopeAsync([FromQuery] FinalizeEnvelopeCommand command)
|
||||
{
|
||||
await Mediator.Send(command.Authorized(User.GetId()));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using AutoMapper;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using EnvelopeGenerator.API.Models;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -12,14 +13,9 @@ using System.Data;
|
||||
using EnvelopeGenerator.Application.Common.SQL;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Server.Options;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller für die Verwaltung von Umschlagempfängern.
|
||||
@@ -27,16 +23,32 @@ namespace EnvelopeGenerator.Server.Controllers;
|
||||
/// <remarks>
|
||||
/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// Konstruktor für den EnvelopeReceiverController.
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logger, IMediator Mediator, IMapper Mapper, IEnvelopeExecutor EnvelopeExecutor, IEnvelopeReceiverExecutor ER_Executor, IDocumentExecutor DocExecutor, IOptions<ConnectionString> csOpt, IOptionsMonitor<SenderFeatureAccessOptions> senderFeatAccessOpt) : ControllerBase
|
||||
public class EnvelopeReceiverController : ControllerBase
|
||||
{
|
||||
private readonly string _cnnStr = csOpt.Value.Value;
|
||||
private readonly IOptionsMonitor<SenderFeatureAccessOptions> SenderFeatAccessOpt = senderFeatAccessOpt;
|
||||
private readonly ILogger<EnvelopeReceiverController> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEnvelopeExecutor _envelopeExecutor;
|
||||
private readonly IEnvelopeReceiverExecutor _erExecutor;
|
||||
private readonly IDocumentExecutor _documentExecutor;
|
||||
private readonly string _cnnStr;
|
||||
|
||||
/// <summary>
|
||||
/// Konstruktor für den EnvelopeReceiverController.
|
||||
/// </summary>
|
||||
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_mapper = mapper;
|
||||
_envelopeExecutor = envelopeExecutor;
|
||||
_erExecutor = erExecutor;
|
||||
_documentExecutor = documentExecutor;
|
||||
_cnnStr = csOpt.Value.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
|
||||
@@ -56,29 +68,11 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
{
|
||||
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
|
||||
|
||||
var result = await Mediator.Send(envelopeReceiver);
|
||||
var result = await _mediator.Send(envelopeReceiver);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("{envelopeKey}")]
|
||||
public async Task<IActionResult> GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
var er = await Mediator.Send(new ReadEnvelopeReceiverQuery()
|
||||
{
|
||||
Key = envelopeKey
|
||||
}, cancel);
|
||||
|
||||
return Ok(er.SingleOrDefault());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
|
||||
/// </summary>
|
||||
@@ -95,67 +89,10 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
[HttpGet("salute")]
|
||||
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
||||
{
|
||||
var name = await Mediator.Send(receiver);
|
||||
var name = await _mediator.Send(receiver);
|
||||
return name is null ? NotFound() : Ok(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeUuid"></param>
|
||||
/// <param name="receiverSignature"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpGet("receiver-envelope-url")]
|
||||
public IActionResult GetReceiverEnvelopeUrl([FromQuery] string envelopeUuid, [FromQuery] string receiverSignature)
|
||||
{
|
||||
if (!CanUseReceiverEnvelopeUrlFeature())
|
||||
return Forbid();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(envelopeUuid) || string.IsNullOrWhiteSpace(receiverSignature))
|
||||
return BadRequest("Envelope uuid and receiver signature are required.");
|
||||
|
||||
var envelopeKey = (envelopeUuid, receiverSignature).ToEnvelopeKey();
|
||||
var absoluteUrl = $"{Request.Scheme}://{Request.Host}/envelope/{Uri.EscapeDataString(envelopeKey)}";
|
||||
|
||||
return Ok(new { url = absoluteUrl });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpGet("receiver-envelope-url/enabled")]
|
||||
public IActionResult GetReceiverEnvelopeUrlFeatureEnabled()
|
||||
{
|
||||
return Ok(new { enabled = CanUseReceiverEnvelopeUrlFeature() });
|
||||
}
|
||||
|
||||
private bool CanUseReceiverEnvelopeUrlFeature()
|
||||
{
|
||||
var senderEmail = User.GetEmail();
|
||||
if (string.IsNullOrWhiteSpace(senderEmail))
|
||||
return false;
|
||||
|
||||
var allowedEmails = SenderFeatAccessOpt.CurrentValue.AllowedReceiverEnvelopeUrlEmails;
|
||||
return allowedEmails.Any(email => string.Equals(email?.Trim(), senderEmail, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns recent receiver suggestions for the authenticated sender.
|
||||
/// </summary>
|
||||
/// <param name="query">Suggestion query options (take 3..5 is applied in handler).</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
/// <returns>List of recent distinct receiver suggestions.</returns>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpGet("recent-suggestions")]
|
||||
public async Task<IActionResult> GetRecentSuggestions([FromQuery] ReadRecentReceiverSuggestionsQuery query, CancellationToken cancel)
|
||||
{
|
||||
var result = await Mediator.Send(query.ForSender(User.GetUsername()), cancel);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
||||
/// </summary>
|
||||
@@ -194,12 +131,12 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||
{
|
||||
#region Create Envelope
|
||||
var envelope = await EnvelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
||||
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
||||
#endregion
|
||||
|
||||
#region Add receivers
|
||||
@@ -208,7 +145,7 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
|
||||
foreach (var receiver in request.Receivers)
|
||||
{
|
||||
var envelopeReceiver = await ER_Executor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
||||
var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
||||
|
||||
if (envelopeReceiver is null)
|
||||
unsentReceivers.Add(receiver);
|
||||
@@ -216,13 +153,13 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
sentReceivers.Add(envelopeReceiver);
|
||||
}
|
||||
|
||||
var res = Mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
||||
var res = _mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
||||
res.UnsentReceivers = unsentReceivers;
|
||||
res.SentReceiver = Mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
||||
res.SentReceiver = _mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
||||
#endregion
|
||||
|
||||
#region Add document
|
||||
var document = await DocExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
||||
var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
||||
|
||||
if (document is null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
|
||||
@@ -244,7 +181,7 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||
|
||||
foreach (var rcv in res.SentReceiver)
|
||||
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<DocReceiverElementCreateDto>())
|
||||
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.Signatures ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.Signature>())
|
||||
{
|
||||
using SqlConnection conn = new(_cnnStr);
|
||||
conn.Open();
|
||||
@@ -258,10 +195,6 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
if (reader.Read())
|
||||
{
|
||||
bool outSuccess = reader.GetBoolean(0);
|
||||
if (!outSuccess)
|
||||
Logger.LogWarning(
|
||||
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||
document.Id, rcv.Id, sign.Page);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@@ -269,6 +202,8 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
#region Create history
|
||||
// ENV_UID, STATUS_ID, USER_ID,
|
||||
string sql_hist = @"
|
||||
USE [DD_ECM]
|
||||
|
||||
DECLARE @OUT_SUCCESS bit;
|
||||
|
||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||
@@ -290,10 +225,6 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
if (reader.Read())
|
||||
{
|
||||
bool outSuccess = reader.GetBoolean(0);
|
||||
if (!outSuccess)
|
||||
Logger.LogWarning(
|
||||
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||
envelope.Uuid);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@@ -322,18 +253,4 @@ public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logg
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
public async Task<IActionResult> RemoveAsync([FromQuery] RemoveEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||
{
|
||||
request = request.Authenticated(User.GetId());
|
||||
await Mediator.Send(request, cancel);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers.Interfaces;
|
||||
namespace EnvelopeGenerator.API.Controllers.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -4,8 +4,9 @@ using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using EnvelopeGenerator.Application.Resources;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
|
||||
@@ -19,6 +20,7 @@ public class LocalizationController : ControllerBase
|
||||
|
||||
private readonly ILogger<LocalizationController> _logger;
|
||||
private readonly IStringLocalizer<Resource> _mLocalizer;
|
||||
private readonly IStringLocalizer<Resource> _localizer;
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
/// <summary>
|
||||
@@ -30,12 +32,14 @@ public class LocalizationController : ControllerBase
|
||||
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
|
||||
public LocalizationController(
|
||||
ILogger<LocalizationController> logger,
|
||||
IStringLocalizer<Resource> localizer,
|
||||
IMemoryCache memoryCache,
|
||||
IStringLocalizer<Resource> modelLocalizer)
|
||||
IStringLocalizer<Resource> _modelLocalizer)
|
||||
{
|
||||
_logger = logger;
|
||||
_localizer = localizer;
|
||||
_cache = memoryCache;
|
||||
_mLocalizer = modelLocalizer;
|
||||
_mLocalizer = _modelLocalizer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -88,10 +92,10 @@ public class LocalizationController : ControllerBase
|
||||
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
||||
|
||||
if (string.IsNullOrEmpty(cookieValue))
|
||||
return "de-DE";
|
||||
return null;
|
||||
|
||||
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
|
||||
return culture?.Value ?? "de-DE";
|
||||
return culture?.Value ?? null;
|
||||
}
|
||||
set
|
||||
{
|
||||
@@ -114,4 +118,4 @@ public class LocalizationController : ControllerBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.Security.Claims;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages read-only envelope sharing flows.
|
||||
@@ -35,36 +33,24 @@ public class ReadOnlyController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new read-only receiver for an envelope.
|
||||
/// Creates a new read-only receiver for the current envelope.
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey">Envelope key (uuid+signature encoded).</param>
|
||||
/// <param name="createDto">Creation payload.</param>
|
||||
[HttpPost("{envelopeKey}")]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
[HttpPost]
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> CreateAsync(
|
||||
[FromRoute] string envelopeKey,
|
||||
[FromBody] EnvelopeReceiverReadOnlyCreateDto createDto,
|
||||
[FromServices] IMediator mediator,
|
||||
CancellationToken cancel)
|
||||
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||
{
|
||||
var envelopeReceiver = await mediator.Send(new ReadEnvelopeReceiverQuery { Key = envelopeKey }, cancel);
|
||||
var authEnvelopeReceiver = envelopeReceiver.FirstOrDefault();
|
||||
|
||||
if (authEnvelopeReceiver?.Envelope is null)
|
||||
return NotFound();
|
||||
|
||||
var addedWho = User.FindFirstValue(ClaimTypes.Email)
|
||||
?? authEnvelopeReceiver.Receiver?.EmailAddress;
|
||||
if (string.IsNullOrWhiteSpace(addedWho))
|
||||
var authReceiverMail = User.GetReceiverMailOfReceiver();
|
||||
if (authReceiverMail is null)
|
||||
{
|
||||
_logger.LogError("Receiver e-mail is missing in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var envelopeId = authEnvelopeReceiver.EnvelopeId;
|
||||
var envelopeId = User.GetEnvelopeIdOfReceiver();
|
||||
|
||||
createDto.AddedWho = addedWho;
|
||||
createDto.AddedWho = authReceiverMail;
|
||||
createDto.EnvelopeId = envelopeId;
|
||||
|
||||
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
|
||||
@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.Receivers.Queries;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller für die Verwaltung von Empfängern.
|
||||
@@ -33,16 +33,15 @@ public class ReceiverController : ControllerBase
|
||||
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
||||
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
||||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery? receiver = null, [FromQuery] bool onlyEmailAddress = false)
|
||||
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
||||
{
|
||||
var result = await _mediator.Send(receiver ?? new ReadReceiverQuery());
|
||||
if (!receiver.HasAnyCriteria)
|
||||
{
|
||||
var all = await _mediator.Send(new ReadReceiverQuery());
|
||||
return Ok(all);
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
return NotFound();
|
||||
else if (onlyEmailAddress)
|
||||
return Ok(result.Select(r => r.EmailAddress).ToList());
|
||||
else
|
||||
return Ok(result);
|
||||
var result = await _mediator.Send(receiver);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Resources;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using EnvelopeGenerator.API.Models;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
@@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
|
||||
@@ -1,9 +1,9 @@
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using EnvelopeGenerator.API.Models;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Documentation;
|
||||
namespace EnvelopeGenerator.API.Documentation;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -16,12 +16,6 @@ public sealed class AuthProxyDocumentFilter : IDocumentFilter
|
||||
/// <param name="swaggerDoc"></param>
|
||||
/// <param name="context"></param>
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
AddLoginOperation(swaggerDoc, context);
|
||||
AddEnvelopeReceiverLoginOperation(swaggerDoc, context);
|
||||
}
|
||||
|
||||
private static void AddLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
const string path = "/api/auth";
|
||||
|
||||
@@ -73,51 +67,4 @@ public sealed class AuthProxyDocumentFilter : IDocumentFilter
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddEnvelopeReceiverLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
const string path = "/api/Auth/envelope-receiver/{key}";
|
||||
|
||||
var bodySchema = context.SchemaGenerator.GenerateSchema(typeof(EnvelopeReceiverLogin), context.SchemaRepository);
|
||||
|
||||
var operation = new OpenApiOperation
|
||||
{
|
||||
Summary = "Envelope receiver login (auth-hub proxy)",
|
||||
Description = "Proxies the envelope receiver login to the auth service. " +
|
||||
"The `cookie` query parameter is always forwarded as `true` so the auth service sets the per-envelope cookie automatically.",
|
||||
Tags = [new() { Name = "Auth" }],
|
||||
Parameters =
|
||||
{
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "key",
|
||||
In = ParameterLocation.Path,
|
||||
Required = true,
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Description = "The unique envelope receiver key."
|
||||
}
|
||||
},
|
||||
RequestBody = new OpenApiRequestBody
|
||||
{
|
||||
Required = false,
|
||||
Content =
|
||||
{
|
||||
["multipart/form-data"] = new OpenApiMediaType { Schema = bodySchema }
|
||||
}
|
||||
},
|
||||
Responses =
|
||||
{
|
||||
["200"] = new OpenApiResponse { Description = "OK – per-envelope cookie set by auth service." },
|
||||
["401"] = new OpenApiResponse { Description = "Unauthorized – invalid or missing access code." }
|
||||
}
|
||||
};
|
||||
|
||||
swaggerDoc.Paths[path] = new OpenApiPathItem
|
||||
{
|
||||
Operations =
|
||||
{
|
||||
[OperationType.Post] = operation
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
17
EnvelopeGenerator.API/EnvelopeClaimTypes.cs
Normal file
17
EnvelopeGenerator.API/EnvelopeClaimTypes.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace EnvelopeGenerator.API;
|
||||
|
||||
/// <summary>
|
||||
/// Provides custom claim types for envelope-related information.
|
||||
/// </summary>
|
||||
public static class EnvelopeClaimTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Claim type for the title of an envelope.
|
||||
/// </summary>
|
||||
public static readonly string Title = $"Envelope{nameof(Title)}";
|
||||
|
||||
/// <summary>
|
||||
/// Claim type for the ID of an envelope.
|
||||
/// </summary>
|
||||
public static readonly string Id = $"Envelope{nameof(Id)}";
|
||||
}
|
||||
76
EnvelopeGenerator.API/EnvelopeGenerator.API.csproj
Normal file
76
EnvelopeGenerator.API/EnvelopeGenerator.API.csproj
Normal file
@@ -0,0 +1,76 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net9.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageId>EnvelopeGenerator.GeneratorAPI</PackageId>
|
||||
<Title></Title>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>EnvelopeGenerator.GeneratorAPI</Product>
|
||||
<Version>1.2.3</Version>
|
||||
<FileVersion>1.2.3</FileVersion>
|
||||
<AssemblyVersion>1.2.3</AssemblyVersion>
|
||||
<PackageOutputPath>Copyright © 2025 Digital Data GmbH. All rights reserved.</PackageOutputPath>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="ClientApp\**" />
|
||||
<Content Remove="ClientApp\**" />
|
||||
<EmbeddedResource Remove="ClientApp\**" />
|
||||
<None Remove="ClientApp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="yarp.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.Scalar" Version="1.1.8" />
|
||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.0" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="System.DirectoryServices" Version="7.0.1" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="7.0.1" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="7.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
101
EnvelopeGenerator.API/Extensions/ReceiverClaimExtensions.cs
Normal file
101
EnvelopeGenerator.API/Extensions/ReceiverClaimExtensions.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
||||
namespace EnvelopeGenerator.API.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for working with envelope-specific authentication claims.
|
||||
/// </summary>
|
||||
public static class ReceiverClaimExtensions
|
||||
{
|
||||
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, string claimType)
|
||||
{
|
||||
var value = user.FindFirstValue(claimType);
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var identity = user.Identity;
|
||||
var principalName = identity?.Name ?? "(anonymous)";
|
||||
var authType = identity?.AuthenticationType ?? "(none)";
|
||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
||||
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope UUID from the claims.
|
||||
/// </summary>
|
||||
public static string GetEnvelopeUuidOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.NameIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver signature from the claims.
|
||||
/// </summary>
|
||||
public static string GetReceiverSignatureOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Hash);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver display name from the claims.
|
||||
/// </summary>
|
||||
public static string GetReceiverNameOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver email address from the claims.
|
||||
/// </summary>
|
||||
public static string GetReceiverMailOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Email);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope title from the claims.
|
||||
/// </summary>
|
||||
public static string GetEnvelopeTitleOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Title);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope identifier from the claims.
|
||||
/// </summary>
|
||||
public static int GetEnvelopeIdOfReceiver(this ClaimsPrincipal user)
|
||||
{
|
||||
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Id);
|
||||
if (!int.TryParse(envIdStr, out var envId))
|
||||
{
|
||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimTypes.Id}' is not a valid integer.");
|
||||
}
|
||||
|
||||
return envId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
|
||||
/// </summary>
|
||||
/// <param name="context">The current HTTP context.</param>
|
||||
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
|
||||
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
|
||||
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
|
||||
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
|
||||
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
|
||||
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
|
||||
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
|
||||
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
|
||||
new(ClaimTypes.Role, receiverRole)
|
||||
};
|
||||
|
||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
var authProperties = new AuthenticationProperties
|
||||
{
|
||||
AllowRefresh = false,
|
||||
IsPersistent = false
|
||||
};
|
||||
|
||||
await context.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(claimsIdentity),
|
||||
authProperties);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Extensions
|
||||
namespace EnvelopeGenerator.API.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace EnvelopeGenerator.Server.Middleware;
|
||||
namespace EnvelopeGenerator.API.Middleware;
|
||||
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Domain.Exceptions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net;
|
||||
@@ -70,21 +69,6 @@ public class ExceptionHandlingMiddleware
|
||||
message = notFoundEx.Message;
|
||||
break;
|
||||
|
||||
case ForbiddenException forbiddenEx:
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
message = forbiddenEx.Message;
|
||||
break;
|
||||
|
||||
case AuthorizationException authorizationEx:
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
message = authorizationEx.Message;
|
||||
break;
|
||||
|
||||
case DataIntegrityException dataIntegrityEx:
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Conflict;
|
||||
message = dataIntegrityEx.Message;
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.LogError(exception, "Unhandled exception occurred.");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
[Obsolete("Use auth DTO")]
|
||||
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||
{
|
||||
public bool HasAccessCode => AccessCode is not null;
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the keys and default values used for authentication token handling
|
||||
/// within the Envelope Generator Server.
|
||||
/// within the Envelope Generator API.
|
||||
/// </summary>
|
||||
public class AuthTokenKeys
|
||||
{
|
||||
12
EnvelopeGenerator.API/Models/ConnectionString.cs
Normal file
12
EnvelopeGenerator.API/Models/ConnectionString.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ConnectionString
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models
|
||||
namespace EnvelopeGenerator.API.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a hyperlink for contact purposes with various HTML attributes.
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
public class Culture
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
public class Cultures : List<Culture>
|
||||
{
|
||||
6
EnvelopeGenerator.API/Models/CustomImages.cs
Normal file
6
EnvelopeGenerator.API/Models/CustomImages.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
public class CustomImages : Dictionary<string, Image>
|
||||
{
|
||||
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
public class ErrorViewModel
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
public class Image
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
public class MainViewModel
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
|
||||
public record Annotation : IAnnotation
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
|
||||
public class AnnotationParams
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
|
||||
/// <summary>
|
||||
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
|
||||
public record Color
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
|
||||
public interface IAnnotation
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the parameters for two-factor authentication (2FA) registration.
|
||||
270
EnvelopeGenerator.API/Program.cs
Normal file
270
EnvelopeGenerator.API/Program.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using DigitalData.Core.API;
|
||||
using DigitalData.Core.Application;
|
||||
using EnvelopeGenerator.Infrastructure;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using Scalar.AspNetCore;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using DigitalData.UserManager.DependencyInjection;
|
||||
using EnvelopeGenerator.Application;
|
||||
using DigitalData.Auth.Client;
|
||||
using DigitalData.Core.Abstractions;
|
||||
using EnvelopeGenerator.API.Models;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||
using EnvelopeGenerator.API.Middleware;
|
||||
using NLog.Web;
|
||||
using NLog;
|
||||
|
||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||
logger.Info("Logging initialized!");
|
||||
|
||||
try
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true);
|
||||
|
||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Host.UseNLog();
|
||||
}
|
||||
|
||||
var config = builder.Configuration;
|
||||
|
||||
var deferredProvider = new DeferredServiceProvider();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||
|
||||
// CORS Policy
|
||||
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
||||
throw new InvalidOperationException("AllowedOrigins section is missing in the configuration.");
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowSpecificOriginsPolicy", builder =>
|
||||
{
|
||||
builder.WithOrigins(allowedOrigins)
|
||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
// Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Version = "v1",
|
||||
Title = "signFLOW Absender-API",
|
||||
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschlägen in der signFLOW-Anwendung.",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Name = "Digital Data GmbH",
|
||||
Url = new Uri("https://digitaldata.works/digitale-signatur#kontakt"),
|
||||
Email = "info-flow@digitaldata.works"
|
||||
},
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Description = "JWT-Autorisierungs-Header unter Verwendung des Bearer-Schemas.",
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
|
||||
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
|
||||
foreach (var xmlFile in xmlFiles)
|
||||
{
|
||||
options.IncludeXmlComments(xmlFile);
|
||||
}
|
||||
|
||||
options.DocumentFilter<EnvelopeGenerator.API.Documentation.AuthProxyDocumentFilter>();
|
||||
});
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
//AddEF Core dbcontext
|
||||
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
|
||||
var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default";
|
||||
var connStr = config.GetConnectionString(cnnStrName)
|
||||
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
|
||||
|
||||
builder.Services.Configure<ConnectionString>(cs => cs.Value = connStr);
|
||||
|
||||
builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
|
||||
|
||||
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"));
|
||||
|
||||
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(opt =>
|
||||
{
|
||||
opt.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
||||
{
|
||||
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
||||
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
||||
return [publicKey.SecurityKey];
|
||||
},
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = authTokenKeys.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = authTokenKeys.Audience,
|
||||
};
|
||||
|
||||
opt.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
// if there is no token read related cookie or query string
|
||||
if (context.Token is null) // if there is no token
|
||||
{
|
||||
if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null)
|
||||
context.Token = cookieToken;
|
||||
else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken))
|
||||
context.Token = queryStrToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Authentication
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
|
||||
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
|
||||
options.LoginPath = "/api/auth/login";
|
||||
options.LogoutPath = "/api/auth/logout";
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy =>
|
||||
policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
||||
.AddPolicy(AuthPolicy.Sender, policy =>
|
||||
policy.RequireRole(Role.Sender))
|
||||
.AddPolicy(AuthPolicy.Receiver, policy =>
|
||||
policy.RequireRole(Role.Receiver.Full))
|
||||
.AddPolicy(AuthPolicy.ReceiverTFA, policy =>
|
||||
policy.RequireRole(Role.Receiver.TFA));
|
||||
|
||||
// User manager
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
builder.Services.AddUserManager<EGDbContext>();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
// LDAP
|
||||
builder.ConfigureBySection<DirectorySearchOptions>();
|
||||
builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions"));
|
||||
|
||||
// Localizer
|
||||
builder.Services.AddCookieBasedLocalizer();
|
||||
|
||||
// Envelope generator serives
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
builder.Services
|
||||
.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
||||
{
|
||||
opt.AddDbTriggerParams(config);
|
||||
opt.AddDbContext((provider, options) =>
|
||||
{
|
||||
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
|
||||
options.UseSqlServer(connStr)
|
||||
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableDetailedErrors();
|
||||
});
|
||||
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
|
||||
})
|
||||
.AddEnvelopeGeneratorServices(config);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
deferredProvider.Factory = () => app.Services;
|
||||
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
|
||||
app.MapOpenApi();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment() || (app.IsDevOrDiP() && config.GetValue<bool>("UseSwagger")))
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.MapScalarApiReference();
|
||||
}
|
||||
|
||||
// Set CORS policy
|
||||
app.UseCors("AllowSpecificOriginsPolicy");
|
||||
|
||||
// Localizer
|
||||
string[] supportedCultureNames = ["de-DE", "en-US"];
|
||||
IList<CultureInfo> list = [.. supportedCultureNames.Select(cn => new CultureInfo(cn))];
|
||||
var cultureInfo = list.FirstOrDefault() ?? throw new InvalidOperationException("There is no supported culture.");
|
||||
var requestLocalizationOptions = new RequestLocalizationOptions
|
||||
{
|
||||
SupportedCultures = list,
|
||||
SupportedUICultures = list
|
||||
};
|
||||
requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
|
||||
app.UseRequestLocalization(requestLocalizationOptions);
|
||||
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapReverseProxy();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Stopped program because of exception");
|
||||
throw;
|
||||
}
|
||||
@@ -4,36 +4,37 @@
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:5131",
|
||||
"sslPort": 8088
|
||||
"applicationUrl": "http://localhost:4176",
|
||||
"sslPort": 44300
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"https (Blazor UI)": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5131",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
|
||||
}
|
||||
},
|
||||
"https (Swagger API)": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"http (Development)": {
|
||||
"angular": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5131",
|
||||
"launchUrl": "",
|
||||
"applicationUrl": "https://localhost:7174;http://localhost:5131",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@@ -41,6 +42,7 @@
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@@ -4,8 +4,7 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
,
|
||||
},
|
||||
"AuthClientParams": {
|
||||
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||
"PublicKeys": [
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"UseSwagger": true,
|
||||
"UseScalar": true,
|
||||
"UseDbMigration": false,
|
||||
"DevEndpoints": {
|
||||
"EnableSignatureInkInProduction": false
|
||||
},
|
||||
"UseDbMigration": true,
|
||||
"DiPMode": true,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
@@ -13,15 +9,9 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:4200",
|
||||
"http://172.24.12.39:9090",
|
||||
"https://localhost:8088",
|
||||
"http://localhost:5131",
|
||||
"http://localhost:7192"
|
||||
],
|
||||
"AllowedOrigins": [ "http://localhost:4200", "http://172.24.12.39:9090", "https://localhost:8088", "http://localhost:5131", "http://localhost:7192" ],
|
||||
"ConnectionStrings": {
|
||||
"Default": "Server=SDD-VHP04-SQL19\\DD_TESTING01;Database=DD_ECM;User Id=sa;Password=123456789dD!;Encrypt=false;TrustServerCertificate=True;",
|
||||
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
|
||||
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||
},
|
||||
"DirectorySearchOptions": {
|
||||
@@ -49,22 +39,6 @@
|
||||
"Issuer": "auth.digitaldata.works",
|
||||
"Audience": "sign-flow.digitaldata.works"
|
||||
},
|
||||
"ApiOptions": {
|
||||
"BaseUrl": ""
|
||||
},
|
||||
"PdfViewerOptions": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"ZoomStepPercentage": 5
|
||||
},
|
||||
"SignatureFieldParams": {
|
||||
"Width": 1.77,
|
||||
"Height": 1.96
|
||||
},
|
||||
"DocumentService": {
|
||||
"BaseUrl": "http://172.24.12.39:9393"
|
||||
},
|
||||
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
|
||||
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.
|
||||
"default-src 'self'",
|
||||
@@ -81,7 +55,7 @@
|
||||
"throwConfigExceptions": true,
|
||||
"variables": {
|
||||
"logDirectory": "E:\\LogFiles\\Digital Data\\signFlow",
|
||||
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Server"
|
||||
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Web"
|
||||
},
|
||||
"targets": {
|
||||
"infoLogs": {
|
||||
@@ -156,17 +130,15 @@
|
||||
"CustomImages": {
|
||||
"App": {
|
||||
"Src": "/img/DD_signFLOW_LOGO.png",
|
||||
"Height": "28px",
|
||||
"Classes": {
|
||||
"Main": "signflow-app-logo"
|
||||
"Main": "signFlow-logo"
|
||||
}
|
||||
},
|
||||
"Company": {
|
||||
"Src": "/img/digital_data.svg",
|
||||
"Height": "33px",
|
||||
"Classes": {
|
||||
"Show": "signflow-company-logo",
|
||||
"Locked": "signflow-company-logo"
|
||||
"Show": "dd-show-logo",
|
||||
"Locked": "dd-locked-logo"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -195,26 +167,13 @@
|
||||
"TimeLimit": "00:30:00"
|
||||
},
|
||||
"DbTriggerParams": {
|
||||
"DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ],
|
||||
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||
"EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||
"History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
||||
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
||||
"Receiver": [],
|
||||
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
||||
},
|
||||
"Cache": {
|
||||
"SignatureCacheExpiration": null,
|
||||
"SqlServer": {
|
||||
"ConnectionString": null,
|
||||
"SchemaName": "dbo",
|
||||
"TableName": "TBDD_CACHE"
|
||||
}
|
||||
},
|
||||
"SenderFeatureAccess": {
|
||||
"AllowedReceiverEnvelopeUrlEmails": ["h.tek@digitaldata.works"]
|
||||
},
|
||||
"MainPageTitle": null,
|
||||
"AnnotationParams": {
|
||||
"Background": {
|
||||
@@ -285,9 +244,5 @@
|
||||
"MarginTopRatio": -0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
"ESignParams": {
|
||||
"ApplicationName": "signFLOW",
|
||||
"DefaultReason": " Bestätigt"
|
||||
}
|
||||
}
|
||||
@@ -10,20 +10,6 @@
|
||||
"Transforms": [
|
||||
{ "PathSet": "/api/auth/sign-flow" }
|
||||
]
|
||||
},
|
||||
"auth-envelope-receiver-login": {
|
||||
"ClusterId": "auth-hub",
|
||||
"Match": {
|
||||
"Path": "/api/Auth/envelope-receiver/{key}",
|
||||
"Methods": [ "POST" ]
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
|
||||
{
|
||||
"QueryValueParameter": "cookie",
|
||||
"Set": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Clusters": {
|
||||
@@ -36,4 +22,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Behaviors;
|
||||
|
||||
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||
|
||||
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
||||
{
|
||||
_validators = validators;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_validators.Any())
|
||||
{
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
var failures = _validators
|
||||
.Select(v => v.Validate(context))
|
||||
.SelectMany(r => r.Errors)
|
||||
.Where(f => f is not null)
|
||||
.ToList();
|
||||
|
||||
if (failures.Count > 0)
|
||||
{
|
||||
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
13
EnvelopeGenerator.Application/Common/CacheKey.cs
Normal file
13
EnvelopeGenerator.Application/Common/CacheKey.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace EnvelopeGenerator.Application.Common;
|
||||
|
||||
// TODO: merge other cache keys here as well, e.g. for templates, etc.
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class CacheKey
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static readonly Guid DefaultConfig = Guid.NewGuid();
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -8,77 +8,42 @@ public record AnnotationCreateDto
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
public int ElementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Not required for DevExpress")]
|
||||
public int ElementId { get; init; } = -1;
|
||||
public string Name { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Not required for DevExpress")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Value { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Not required for DevExpress")]
|
||||
public string Value { get; init; } = string.Empty;
|
||||
public string Type { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Not required for DevExpress")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal position of the signature field on the page.
|
||||
/// <br/><br/>
|
||||
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, X increases to the right.
|
||||
/// <br/>
|
||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||
/// Convert: <c>xDX = xInches * 100.0</c>
|
||||
/// <br/>
|
||||
/// <b>Conversion to PDF Points:</b> Multiply by 72 (PSPDFKit, iText7 use 1/72 inch).
|
||||
/// Convert: <c>xPt = xInches * 72.0</c>
|
||||
/// </summary>
|
||||
public double? X { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vertical position of the signature field on the page.
|
||||
/// <br/><br/>
|
||||
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, Y increases downward.
|
||||
/// <br/>
|
||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||
/// Convert: <c>yDX = yInches * 100.0</c>
|
||||
/// <br/>
|
||||
/// <b>Conversion to PDF Points (top-left origin):</b> Multiply by 72.
|
||||
/// Convert: <c>yPt = yInches * 72.0</c>
|
||||
/// <br/>
|
||||
/// <b>Conversion to PDF Points (bottom-left origin - iText7):</b> Y-flip required.
|
||||
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
||||
///
|
||||
/// </summary>
|
||||
public double? Y { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Not required for DevExpress")]
|
||||
public double? Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Not required for DevExpress")]
|
||||
public double? Height { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Added to eliminate the need for SignatureDto in DevExpress
|
||||
/// </summary>
|
||||
public int? Page { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing configuration settings.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ConfigDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default document path.
|
||||
/// </summary>
|
||||
public string? DocumentPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sending profile identifier.
|
||||
/// </summary>
|
||||
@@ -23,5 +31,30 @@ public class ConfigDto
|
||||
/// <summary>
|
||||
/// Gets or sets the path where exports will be saved.
|
||||
/// </summary>
|
||||
public string? ExportPath { get; set; }
|
||||
public string ExportPath { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the creation timestamp.
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last update timestamp.
|
||||
/// </summary>
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the legacy tinyint GUID field.
|
||||
/// </summary>
|
||||
public byte Guid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether default TFA is enabled.
|
||||
/// </summary>
|
||||
public bool DefTfaEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether default TFA uses phone.
|
||||
/// </summary>
|
||||
public bool DefTfaWithPhone { get; set; }
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing document metadata and elements without binary payload.
|
||||
/// </summary>
|
||||
public record DocumentDetailsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the document.
|
||||
/// </summary>
|
||||
public int Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the envelope ID to which the document belongs.
|
||||
/// </summary>
|
||||
public int EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the document was added.
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||
/// </summary>
|
||||
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
|
||||
}
|
||||
@@ -1,27 +1,35 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||
/// </summary>
|
||||
public record DocumentDto : DocumentDetailsDto
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocumentDto
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the unique identifier of the document.
|
||||
/// </summary>
|
||||
public DocumentDto() { }
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the envelope ID to which the document belongs.
|
||||
/// </summary>
|
||||
/// <param name="byteData"></param>
|
||||
/// <param name="details"></param>
|
||||
public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
|
||||
{
|
||||
ByteData = byteData;
|
||||
}
|
||||
public int EnvelopeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the document was added.
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the binary data of the document, if available.
|
||||
/// </summary>
|
||||
public byte[]? ByteData { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||
/// </summary>
|
||||
public IEnumerable<SignatureDto>? Elements { get; set; }
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocumentStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||
using DigitalData.UserManager.Application.DTOs.User;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeDto : IEnvelope
|
||||
{
|
||||
/// <summary>
|
||||
@@ -43,6 +45,16 @@ public record EnvelopeDto : IEnvelope
|
||||
[TemplatePlaceholder("[MESSAGE]")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DateTime? ExpiresWhen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DateTime? ExpiresWarningWhen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -59,10 +71,15 @@ public record EnvelopeDto : IEnvelope
|
||||
[TemplatePlaceholder("[DOCUMENT_TITLE]")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Default value is string.Empty
|
||||
/// </summary>
|
||||
public string? Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public ContractType? ContractType { get; set; }
|
||||
public int? ContractType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default value is 'de-DE'
|
||||
@@ -72,34 +89,7 @@ public record EnvelopeDto : IEnvelope
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? EnvelopeTypeId { get; set; }
|
||||
|
||||
// TODO: use ReadAndConfirm property name
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Use EnvelopeExtensions.IsReadAndConfirm extension metot instead.")]
|
||||
public bool ReadOnly => this.IsReadAndConfirm();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public CertificationType? CertificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? FinalEmailToCreator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? FinalEmailToReceivers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool? SendReminderEmails { get; set; }
|
||||
public bool SendReminderEmails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -111,6 +101,36 @@ public record EnvelopeDto : IEnvelope
|
||||
/// </summary>
|
||||
public int? ReminderIntervalDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? EnvelopeTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool ReadOnly => EnvelopeTypeId == 2;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? CertificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool UseAccessCode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? FinalEmailToCreator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int? FinalEmailToReceivers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -121,11 +141,6 @@ public record EnvelopeDto : IEnvelope
|
||||
/// </summary>
|
||||
public int? ExpiresWarningWhenDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool UseAccessCode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -165,9 +180,4 @@ public record EnvelopeDto : IEnvelope
|
||||
///
|
||||
/// </summary>
|
||||
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||
/// </summary>
|
||||
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
@@ -60,11 +61,6 @@ public record EnvelopeReceiverDto
|
||||
/// </summary>
|
||||
public string? PrivateMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? AccessCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -79,13 +75,4 @@ public record EnvelopeReceiverDto
|
||||
///
|
||||
/// </summary>
|
||||
public bool HasPhoneNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this receiver has signed the envelope.
|
||||
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
|
||||
/// </summary>
|
||||
public bool Signed => Envelope?.Histories?.Any(h =>
|
||||
h.Receiver?.Id == ReceiverId &&
|
||||
h.Status == EnvelopeStatus.DocumentSigned
|
||||
) ?? false;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="DateValid"></param>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||
DateTime DateValid)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
|
||||
@@ -6,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||
/// Contains information about the receiver, associated envelope, and audit details.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeReceiverReadOnlyDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeTypeDto
|
||||
{
|
||||
/// <summary>
|
||||
@@ -30,7 +31,7 @@ public class EnvelopeTypeDto
|
||||
/// <summary>
|
||||
/// Gets or sets the certification type identifier.
|
||||
/// </summary>
|
||||
public CertificationType? CertificationType { get; set; }
|
||||
public int? CertificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether an access code is required.
|
||||
@@ -80,5 +81,5 @@ public class EnvelopeTypeDto
|
||||
/// <summary>
|
||||
/// Gets or sets the contract type associated with the envelope type.
|
||||
/// </summary>
|
||||
public ContractType? ContractType { get; set; }
|
||||
public int? ContractType { get; set; }
|
||||
}
|
||||
@@ -7,9 +7,7 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the AutoMapper profile configuration for mapping between
|
||||
@@ -25,40 +23,34 @@ public class MappingProfile : Profile
|
||||
{
|
||||
// Entity to DTO mappings
|
||||
CreateMap<Config, ConfigDto>();
|
||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||
CreateMap<Signature, SignatureDto>();
|
||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>()
|
||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||
CreateMap<Envelope, EnvelopeDto>();
|
||||
CreateMap<Document, DocumentDto>();
|
||||
CreateMap<Document, DocumentDetailsDto>();
|
||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||
CreateMap<Receiver, ReceiverDto>();
|
||||
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||
|
||||
// DTO to Entity mappings
|
||||
CreateMap<ConfigDto, Config>();
|
||||
CreateMap<DocReceiverElementDto, DocReceiverElement>();
|
||||
CreateMap<Signature, DocReceiverElement>()
|
||||
.ForMember(dest => dest.Ink, opt => opt.MapFrom(src => src.DataUrl.MapDataUrlToRequiredBytes()))
|
||||
.MapChangedWhen();
|
||||
CreateMap<SignatureDto, Signature>();
|
||||
CreateMap<DocumentStatusDto, DocumentStatus>();
|
||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||
CreateMap<EnvelopeDto, Envelope>();
|
||||
CreateMap<DocumentDto, Document>();
|
||||
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
||||
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||
.MapAddedWhen();
|
||||
|
||||
@@ -69,4 +61,4 @@ public class MappingProfile : Profile
|
||||
? new SmsResponse() { Ok = true }
|
||||
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record SmsResponse
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
public sealed class PdfPageImageDto
|
||||
{
|
||||
public int PageNumber { get; init; }
|
||||
|
||||
public double WidthPt { get; init; }
|
||||
|
||||
public double HeightPt { get; init; }
|
||||
|
||||
public int WidthPx { get; init; }
|
||||
|
||||
public int HeightPx { get; init; }
|
||||
|
||||
public string ImageDataUrl { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
public sealed class PdfRenderRequestDto
|
||||
{
|
||||
public byte[]? PdfBytes { get; init; }
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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);
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Represents PSPDFKit annotation data.
|
||||
/// </summary>
|
||||
/// <param name="Instant">Instant annotation data.</param>
|
||||
/// <param name="Structured">Structured annotation data.</param>
|
||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
||||
@@ -1,4 +1,5 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
@@ -6,6 +7,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
||||
/// This model holds the signature image (as base64 data URL) along with signer information
|
||||
/// used for rendering applied signatures on the PDF canvas.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
|
||||
/// <br/>
|
||||
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
|
||||
/// </remarks>
|
||||
public sealed record Signature
|
||||
{
|
||||
/// <summary>
|
||||
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
|
||||
/// </summary>
|
||||
public required int Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded data URL of the signature image.
|
||||
/// <br/>
|
||||
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
|
||||
/// <br/>
|
||||
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
|
||||
/// <br/>
|
||||
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
|
||||
/// </summary>
|
||||
public required string DataUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Full name of the signer (first and last name).
|
||||
/// <br/>
|
||||
/// <b>Required:</b> Yes (validated in popup)
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Max Mustermann"
|
||||
/// </summary>
|
||||
public required string FullName { get; init; }
|
||||
|
||||
private readonly string? _position = null;
|
||||
|
||||
/// <summary>
|
||||
/// Job title or position of the signer.
|
||||
/// <br/>
|
||||
/// <b>Required:</b> No (optional field)
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Geschäftsführer" or empty string
|
||||
/// </summary>
|
||||
public string? Position
|
||||
{
|
||||
get => _position;
|
||||
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Location/place where the signature was created.
|
||||
/// <br/>
|
||||
/// <b>Required:</b> Yes (validated in popup)
|
||||
/// <br/>
|
||||
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||
/// </summary>
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
// TODO: Add the “Reason” property and integrate with UI and the digital signature process.
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||
/// </summary>
|
||||
public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class SignatureDto : ISignature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the element.
|
||||
@@ -20,21 +19,11 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
||||
/// </summary>
|
||||
public int DocumentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public virtual DocumentDto? Document { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the receiver.
|
||||
/// </summary>
|
||||
public int ReceiverId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public virtual ReceiverDto? Receiver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the element.
|
||||
/// </summary>
|
||||
@@ -104,71 +93,4 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
||||
/// Gets or sets the left position of the element (in layout terms).
|
||||
/// </summary>
|
||||
public double Left => X;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IEnumerable<AnnotationDto>? Annotations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? FullName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Place { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public byte[]? Ink { get; set; }
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
|
||||
/// </summary>
|
||||
public sealed record UserPreferencesDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The active DevExpress theme name.
|
||||
/// Allowed values: "blazing-berry", "fluent", "purple"
|
||||
/// </summary>
|
||||
public string ThemeName { get; set; } = "blazing-berry";
|
||||
|
||||
/// <summary>
|
||||
/// Whether dark mode is active.
|
||||
/// Default: false (light mode).
|
||||
/// </summary>
|
||||
public bool IsDarkMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
|
||||
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
|
||||
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
|
||||
/// </summary>
|
||||
public Dictionary<string, string> GridLayouts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The authenticated user's display name ("{GivenName} {Surname}").
|
||||
/// Populated by the server on GET responses from JWT claims.
|
||||
/// NOT persisted in cache — injected at read time by UserPreferencesController.
|
||||
/// Ignored on PUT requests.
|
||||
/// </summary>
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static UserPreferencesDto Default { get; } = new UserPreferencesDto();
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
using AutoMapper;
|
||||
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
||||
using System;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
||||
|
||||
@@ -14,29 +13,12 @@ public static class AutoMapperAuditingExtensions
|
||||
/// </summary>
|
||||
public static IMappingExpression<TSource, TDestination> MapAddedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
||||
where TDestination : IHasAddedWhen
|
||||
=> expression.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
||||
=> expression.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
|
||||
|
||||
/// <summary>
|
||||
/// Maps <see cref="IHasChangedWhen.ChangedWhen"/> to the current UTC time.
|
||||
/// </summary>
|
||||
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
||||
where TDestination : IHasChangedWhen
|
||||
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
||||
|
||||
/// <summary>
|
||||
/// Converts a base64 data URL string to a byte array.
|
||||
/// Handles data URLs in the format: "data:image/png;base64,iVBORw0KG..."
|
||||
/// </summary>
|
||||
/// <param name="dataUrl">The base64 data URL string from Canvas.toDataURL()</param>
|
||||
/// <returns>The decoded byte array, or null if the input is null or empty</returns>
|
||||
public static byte[]? MapDataUrlToRequiredBytes(this string dataUrl)
|
||||
{
|
||||
// Remove data URL prefix (e.g., "data:image/png;base64,")
|
||||
var base64Index = dataUrl.IndexOf(',', StringComparison.Ordinal);
|
||||
if (base64Index == -1)
|
||||
throw new ArgumentException("Invalid data URL format. Unable to extract base64 data.", nameof(dataUrl));
|
||||
|
||||
var base64Data = dataUrl[(base64Index + 1)..];
|
||||
return Convert.FromBase64String(base64Data);
|
||||
}
|
||||
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IPdfPageImageRenderer
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
Stream ComposeSignaturePreviewPng(ISignatureInkPayload element);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsSupportedImageFormat(byte[] bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class PdfPageImageRendererExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="renderer"></param>
|
||||
/// <param name="inkBytes"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="BadRequestException"></exception>
|
||||
public static byte[] EnsureSupportedImageBytes(this IPdfPageImageRenderer renderer, byte[] inkBytes)
|
||||
{
|
||||
if (!renderer.IsSupportedImageFormat(inkBytes))
|
||||
throw new BadRequestException("Only PNG and JPEG signature formats are supported.");
|
||||
|
||||
return inkBytes;
|
||||
}
|
||||
|
||||
public static byte[] ComposeSignaturePreviewPngBytes(this IPdfPageImageRenderer renderer, ISignatureInkPayload element)
|
||||
{
|
||||
using var stream = renderer.ComposeSignaturePreviewPng(element);
|
||||
|
||||
if (stream is MemoryStream memoryStream)
|
||||
return memoryStream.ToArray();
|
||||
|
||||
using var copy = new MemoryStream();
|
||||
stream.CopyTo(copy);
|
||||
return copy.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IRequestContext
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
string? Origin { get; }
|
||||
}
|
||||
@@ -6,6 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Use IRepository")]
|
||||
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int>
|
||||
public interface IDocumentReceiverElementRepository : ICRUDRepository<Signature, int>
|
||||
{
|
||||
}
|
||||
@@ -8,6 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("Use MediatR")]
|
||||
public interface IDocumentReceiverElementService : IBasicCRUDService<DocReceiverElementDto, DocReceiverElement, int>
|
||||
public interface IDocumentReceiverElementService : IBasicCRUDService<SignatureDto, Signature, int>
|
||||
{
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
|
||||
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
|
||||
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||
/// </summary>
|
||||
public interface IUserPreferencesService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves preferences for the given user.
|
||||
/// Always returns a valid object — never throws, never returns null.
|
||||
/// Returns defaults if no entry exists in cache.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Persists the given preferences for the user in the distributed cache.
|
||||
/// Failures are non-fatal and silently ignored.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="preferences">The preferences to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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]
|
||||
|
||||
*/
|
||||
@@ -1,19 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,37 +1,88 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
|
||||
/// <summary>
|
||||
/// Notification raised when a document is signed by a receiver.
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||
public record DocSignedNotification : INotification, ISendMailNotification
|
||||
/// <param name="Instant"></param>
|
||||
/// <param name="Structured"></param>
|
||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Original"></param>
|
||||
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification, ISendMailNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// The envelope receiver information.
|
||||
///
|
||||
/// </summary>
|
||||
public required EnvelopeReceiverDto EnvelopeReceiver { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The PSPDFKit annotation data.
|
||||
/// </summary>
|
||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the email template type.
|
||||
///
|
||||
/// </summary>
|
||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the email address of the receiver.
|
||||
///
|
||||
/// </summary>
|
||||
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||
public string EmailAddress => Receiver?.EmailAddress
|
||||
?? throw new InvalidOperationException($"Receiver is null." +
|
||||
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class DocSignedNotificationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
||||
/// </summary>
|
||||
/// <param name="dto">The DTO to convert.</param>
|
||||
/// <param name="psPdfKitAnnotation"></param>
|
||||
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
|
||||
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="dtoTask"></param>
|
||||
/// <param name="psPdfKitAnnotation"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
|
||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="publisher"></param>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await publisher.Publish(notification, cancel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await publisher.Publish(new RemoveSignatureNotification()
|
||||
{
|
||||
EnvelopeId = notification.EnvelopeId,
|
||||
ReceiverId = notification.ReceiverId
|
||||
}, cancel);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
@@ -9,7 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private const string BlankAnnotationJson = "{}";
|
||||
@@ -31,11 +29,10 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
||||
{
|
||||
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||
ReceiverId = notification.EnvelopeReceiver.ReceiverId,
|
||||
EnvelopeId = notification.EnvelopeId,
|
||||
ReceiverId = notification.ReceiverId,
|
||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||
: BlankAnnotationJson
|
||||
|
||||
@@ -29,13 +29,13 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <returns></returns>
|
||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
{
|
||||
if (notification.EnvelopeReceiver.Receiver is null)
|
||||
if (notification.Receiver is null)
|
||||
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
||||
|
||||
await _sender.Send(new CreateHistoryCommand()
|
||||
{
|
||||
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||
UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress,
|
||||
EnvelopeId = notification.EnvelopeId,
|
||||
UserReference = notification.Receiver.EmailAddress,
|
||||
Status = EnvelopeStatus.DocumentSigned,
|
||||
}, cancel);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
||||
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
||||
{
|
||||
emailOut.ReferenceString = notification.EmailAddress;
|
||||
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||
emailOut.ReferenceId = notification.ReceiverId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,11 +42,11 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
||||
{
|
||||
var placeHolders = new Dictionary<string, string>()
|
||||
{
|
||||
{ "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? string.Empty },
|
||||
{ "[DOCUMENT_TITLE]", notification.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
|
||||
{ "[NAME_RECEIVER]", notification.Name ?? string.Empty },
|
||||
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
|
||||
};
|
||||
|
||||
if (notification.EnvelopeReceiver.Envelope.IsReadAndConfirm())
|
||||
if (notification.Envelope.IsReadAndConfirm())
|
||||
{
|
||||
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
||||
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||
|
||||
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||
{
|
||||
public required Envelope Envelope { get; init; }
|
||||
|
||||
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||
|
||||
public required string Reason { get; init; }
|
||||
|
||||
public required string SenderReference { get; init; }
|
||||
|
||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||
|
||||
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||
|
||||
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||
{
|
||||
public SendDeletedMailHandler(
|
||||
IRepository<EmailTemplate> tempRepo,
|
||||
IRepository<EmailOut> emailOutRepo,
|
||||
IOptions<MailParams> mailParamsOptions,
|
||||
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||
{
|
||||
emailOut.ReferenceString = notification.EmailAddress;
|
||||
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||
}
|
||||
|
||||
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||
{
|
||||
var senderName = notification.Envelope.User?.Email;
|
||||
if (string.IsNullOrWhiteSpace(senderName))
|
||||
senderName = notification.SenderReference;
|
||||
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||
["[REASON]"] = notification.Reason
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
@@ -97,10 +96,6 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
|
||||
{
|
||||
// do not send email if the envelope is deleted and the status is less than or equal to EnvelopeSaved
|
||||
if (notification is EnvelopeDeletedNotification delNot && delNot.Envelope.Status <= EnvelopeStatus.EnvelopeSaved)
|
||||
return;
|
||||
|
||||
var placeHolders = CreatePlaceHolders(notification);
|
||||
|
||||
var temp = await TempRepo
|
||||
@@ -118,7 +113,7 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
||||
EmailAddress = notification.EmailAddress,
|
||||
EmailBody = temp.Body,
|
||||
EmailSubj = temp.Subject,
|
||||
AddedWhen = DateTime.Now,
|
||||
AddedWhen = DateTime.UtcNow,
|
||||
AddedWho = DispatcherParams.AddedWho,
|
||||
SendingProfile = DispatcherParams.SendingProfile,
|
||||
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
||||
|
||||
@@ -14,4 +14,10 @@ public record EnvelopeQueryBase
|
||||
/// Die universell eindeutige Kennung des Umschlags.
|
||||
/// </summary>
|
||||
public virtual string? Uuid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wenn gesetzt, wird das DocResult-Feld in der Abfrage einbezogen.
|
||||
/// Standardmäßig wird DocResult nicht geladen, um große Binärdaten zu vermeiden.
|
||||
/// </summary>
|
||||
public bool IncludeDocResult { get; set; } = false;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Query;
|
||||
namespace EnvelopeGenerator.Application.Common.Query;
|
||||
|
||||
/// <summary>
|
||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||
@@ -31,6 +29,5 @@ public record ReceiverQueryBase
|
||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||
/// </remarks>
|
||||
[NotMapped]
|
||||
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user