Compare commits
5 Commits
feat/edit-
...
feat/wisag
| Author | SHA1 | Date | |
|---|---|---|---|
| 7271106892 | |||
| d02149cb6b | |||
| 8d8b9a6ea0 | |||
| 96a2abdda8 | |||
| 6cc2d77fc0 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -365,8 +365,3 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
||||||
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
||||||
/EnvelopeGenerator.Tests.Application/annotations.json
|
/EnvelopeGenerator.Tests.Application/annotations.json
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
|
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
|
||||||
/ignore/EnvelopeGenerator.Form
|
|
||||||
|
|||||||
316
AGENTS.md
316
AGENTS.md
@@ -1,316 +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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Deployment Docs
|
|
||||||
|
|
||||||
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
|
||||||
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
|
|
||||||
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
|
|
||||||
**When you need it:**
|
|
||||||
- Deploying to IIS for the first time
|
|
||||||
- Troubleshooting 500 errors on production after deploy
|
|
||||||
- Setting up a new deployment environment
|
|
||||||
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Active Architecture (Post-Migration)
|
|
||||||
|
|
||||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
|
||||||
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
|
||||||
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
|
||||||
|
|
||||||
**Backend + 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}`
|
|
||||||
|
|
||||||
## 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_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
|
|
||||||
- `TBSIG_RECEIVER` - Receiver registry
|
|
||||||
- `TBSIG_DOCUMENT` - PDF binary data
|
|
||||||
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
|
||||||
|
|
||||||
## DevExpress
|
|
||||||
|
|
||||||
**License:** Commercial (v25.2.3)
|
|
||||||
**Components Used:**
|
|
||||||
- `DxGrid` - Master-detail grids
|
|
||||||
- `DxPdfViewer` - Server-side PDF rendering
|
|
||||||
- `DxPopup` - Modal dialogs
|
|
||||||
- `DxToolbar` - Action bars
|
|
||||||
- `DxFormLayout` - Forms
|
|
||||||
|
|
||||||
**Theme:** Blazing Berry (default)
|
|
||||||
|
|
||||||
## JavaScript Interop
|
|
||||||
|
|
||||||
**PDF Viewer:** `wwwroot/js/pdf-viewer.js`
|
|
||||||
```javascript
|
|
||||||
window.pdfViewer = {
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
|
||||||
renderPage(num),
|
|
||||||
renderSignatureButtons(signatures, pageNum, dotNetRef),
|
|
||||||
applySignature(signatureId, dataUrl, fullName, position, place),
|
|
||||||
zoomIn(), zoomOut(), dispose()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Signature Pad:** `wwwroot/js/receiver-signature.js`
|
|
||||||
```javascript
|
|
||||||
window.receiverSignature = {
|
|
||||||
initializeDrawPad(canvasId, dotNetRef),
|
|
||||||
getSignatureDataUrl(canvasId),
|
|
||||||
clearPad(canvasId)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multi-Envelope Support
|
|
||||||
|
|
||||||
Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies:
|
|
||||||
```
|
|
||||||
AuthTokenSignFLOWReceiver.{envelopeKey}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each envelope maintains independent authentication state.
|
|
||||||
|
|
||||||
## External Dependencies
|
|
||||||
|
|
||||||
**CDN:**
|
|
||||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
|
||||||
|
|
||||||
**NuGet (Server.Client):**
|
|
||||||
- `DevExpress.Blazor.*` 25.2.3
|
|
||||||
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
|
||||||
|
|
||||||
**External Services:**
|
|
||||||
- LDAP/AD authentication (optional)
|
|
||||||
- GTX Messaging (SMS 2FA)
|
|
||||||
- Email dispatcher (signFlow)
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
None required. All config in `appsettings.json`.
|
|
||||||
|
|
||||||
**Local dev ports:**
|
|
||||||
- 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,21 +1,19 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.DTO;
|
using DigitalData.Core.Abstraction.Application.DTO;
|
||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Server.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
using EnvelopeGenerator.Application.Histories.Queries;
|
using EnvelopeGenerator.Application.Histories.Queries;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages annotations and signature lifecycle for envelopes.
|
/// Manages annotations and signature lifecycle for envelopes.
|
||||||
@@ -61,8 +59,8 @@ public class AnnotationController : ControllerBase
|
|||||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var signature = User.ReceiverSignature();
|
var signature = User.GetReceiverSignatureOfReceiver();
|
||||||
var uuid = User.EnvelopeUuid();
|
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||||
|
|
||||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
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))
|
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||||
|
|
||||||
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
var docSignedNotification = await _mediator
|
||||||
var docSignedNotification = envelopeReceiverDto is not null
|
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||||
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
.ToDocSignedNotification(psPdfKitAnnotation)
|
||||||
: throw new NotFoundException("Envelope receiver is not found.");
|
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||||
|
|
||||||
try
|
await _mediator.PublishSafely(docSignedNotification, cancel);
|
||||||
{
|
|
||||||
await _mediator.Publish(docSignedNotification, cancel);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await _mediator.Publish(new RemoveSignatureNotification()
|
|
||||||
{
|
|
||||||
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
|
|
||||||
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
|
|
||||||
}, cancel);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -106,9 +92,9 @@ public class AnnotationController : ControllerBase
|
|||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||||
{
|
{
|
||||||
var signature = User.ReceiverSignature();
|
var signature = User.GetReceiverSignatureOfReceiver();
|
||||||
var uuid = User.EnvelopeUuid();
|
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||||
var mail = User.ReceiverMail();
|
var mail = User.GetReceiverMailOfReceiver();
|
||||||
|
|
||||||
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exposes configuration data required by the client applications.
|
/// 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.Domain.Constants;
|
||||||
using EnvelopeGenerator.Application.EmailTemplates.Queries;
|
using EnvelopeGenerator.Application.EmailTemplates.Queries;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller for managing temp templates.
|
/// Controller for managing temp templates.
|
||||||
@@ -24,7 +24,7 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
/// </param>
|
/// </param>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize(Policy = AuthPolicy.Sender)]
|
||||||
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.API.Extensions;
|
||||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
|
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
|
||||||
@@ -27,8 +27,6 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class EnvelopeController : ControllerBase
|
public class EnvelopeController : ControllerBase
|
||||||
{
|
{
|
||||||
public record DeleteEnvelopeRequest(string Reason);
|
|
||||||
|
|
||||||
private readonly ILogger<EnvelopeController> _logger;
|
private readonly ILogger<EnvelopeController> _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
@@ -53,7 +51,7 @@ public class EnvelopeController : ControllerBase
|
|||||||
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
||||||
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
||||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||||
{
|
{
|
||||||
@@ -95,11 +93,12 @@ public class EnvelopeController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="command"></param>
|
/// <param name="command"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[NonAction]
|
||||||
|
[Authorize]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
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)
|
if (res is null)
|
||||||
{
|
{
|
||||||
@@ -109,18 +108,4 @@ public class EnvelopeController : ControllerBase
|
|||||||
else
|
else
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
|
||||||
[HttpDelete("{id:int}")]
|
|
||||||
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
|
||||||
{
|
|
||||||
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
|
||||||
{
|
|
||||||
EnvelopeId = id,
|
|
||||||
Reason = request?.Reason ?? string.Empty
|
|
||||||
}.WithAuth(User.GetId()));
|
|
||||||
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
|||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Server.Models;
|
using EnvelopeGenerator.API.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -13,10 +13,9 @@ using System.Data;
|
|||||||
using EnvelopeGenerator.Application.Common.SQL;
|
using EnvelopeGenerator.Application.Common.SQL;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.API.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller für die Verwaltung von Umschlagempfängern.
|
/// Controller für die Verwaltung von Umschlagempfängern.
|
||||||
@@ -74,24 +73,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
return Ok(result);
|
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>
|
/// <summary>
|
||||||
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
|
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -150,7 +131,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
||||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</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>
|
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
@@ -200,7 +181,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||||
|
|
||||||
foreach (var rcv in res.SentReceiver)
|
foreach (var rcv in res.SentReceiver)
|
||||||
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.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);
|
using SqlConnection conn = new(_cnnStr);
|
||||||
conn.Open();
|
conn.Open();
|
||||||
@@ -214,10 +195,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
if (!outSuccess)
|
|
||||||
_logger.LogWarning(
|
|
||||||
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
|
||||||
document.Id, rcv.Id, sign.Page);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -225,6 +202,8 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
#region Create history
|
#region Create history
|
||||||
// ENV_UID, STATUS_ID, USER_ID,
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
string sql_hist = @"
|
string sql_hist = @"
|
||||||
|
USE [DD_ECM]
|
||||||
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
@@ -246,10 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
if (!outSuccess)
|
|
||||||
_logger.LogWarning(
|
|
||||||
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
|
||||||
envelope.Uuid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -6,7 +6,7 @@ using EnvelopeGenerator.Application.Histories.Queries;
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
|
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers.Interfaces;
|
namespace EnvelopeGenerator.API.Controllers.Interfaces;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -4,8 +4,9 @@ using Microsoft.AspNetCore.Localization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
|
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
|
||||||
@@ -19,6 +20,7 @@ public class LocalizationController : ControllerBase
|
|||||||
|
|
||||||
private readonly ILogger<LocalizationController> _logger;
|
private readonly ILogger<LocalizationController> _logger;
|
||||||
private readonly IStringLocalizer<Resource> _mLocalizer;
|
private readonly IStringLocalizer<Resource> _mLocalizer;
|
||||||
|
private readonly IStringLocalizer<Resource> _localizer;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -30,12 +32,14 @@ public class LocalizationController : ControllerBase
|
|||||||
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
|
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
|
||||||
public LocalizationController(
|
public LocalizationController(
|
||||||
ILogger<LocalizationController> logger,
|
ILogger<LocalizationController> logger,
|
||||||
|
IStringLocalizer<Resource> localizer,
|
||||||
IMemoryCache memoryCache,
|
IMemoryCache memoryCache,
|
||||||
IStringLocalizer<Resource> modelLocalizer)
|
IStringLocalizer<Resource> _modelLocalizer)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_localizer = localizer;
|
||||||
_cache = memoryCache;
|
_cache = memoryCache;
|
||||||
_mLocalizer = modelLocalizer;
|
_mLocalizer = _modelLocalizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -88,10 +92,10 @@ public class LocalizationController : ControllerBase
|
|||||||
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(cookieValue))
|
if (string.IsNullOrEmpty(cookieValue))
|
||||||
return "de-DE";
|
return null;
|
||||||
|
|
||||||
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
|
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
|
||||||
return culture?.Value ?? "de-DE";
|
return culture?.Value ?? null;
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
@@ -114,4 +118,4 @@ public class LocalizationController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,12 @@ using DigitalData.Core.Abstraction.Application.DTO;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.API.Extensions;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages read-only envelope sharing flows.
|
/// Manages read-only envelope sharing flows.
|
||||||
@@ -41,14 +41,14 @@ public class ReadOnlyController : ControllerBase
|
|||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||||
{
|
{
|
||||||
var authReceiverMail = User.ReceiverMail();
|
var authReceiverMail = User.GetReceiverMailOfReceiver();
|
||||||
if (authReceiverMail is null)
|
if (authReceiverMail is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
var envelopeId = User.EnvelopeId();
|
var envelopeId = User.GetEnvelopeIdOfReceiver();
|
||||||
|
|
||||||
createDto.AddedWho = authReceiverMail;
|
createDto.AddedWho = authReceiverMail;
|
||||||
createDto.EnvelopeId = envelopeId;
|
createDto.EnvelopeId = envelopeId;
|
||||||
@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller für die Verwaltung von Empfängern.
|
/// 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>
|
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
||||||
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
||||||
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery? receiver = null, [FromQuery] bool onlyEmailAddress = false)
|
|
||||||
{
|
{
|
||||||
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)
|
var result = await _mediator.Send(receiver);
|
||||||
return NotFound();
|
return result is null ? NotFound() : Ok(result);
|
||||||
else if (onlyEmailAddress)
|
|
||||||
return Ok(result.Select(r => r.EmailAddress).ToList());
|
|
||||||
else
|
|
||||||
return Ok(result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.Common.Extensions;
|
|||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using EnvelopeGenerator.Application.Resources;
|
using EnvelopeGenerator.Application.Resources;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Server.Models;
|
using EnvelopeGenerator.API.Models;
|
||||||
using Ganss.Xss;
|
using Ganss.Xss;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
@@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
|
/// 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.Any;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Documentation;
|
namespace EnvelopeGenerator.API.Documentation;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -16,12 +16,6 @@ public sealed class AuthProxyDocumentFilter : IDocumentFilter
|
|||||||
/// <param name="swaggerDoc"></param>
|
/// <param name="swaggerDoc"></param>
|
||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
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";
|
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;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Extensions
|
namespace EnvelopeGenerator.API.Extensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
|
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Server.Middleware;
|
namespace EnvelopeGenerator.API.Middleware;
|
||||||
|
|
||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.Core.Exceptions;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -69,11 +69,6 @@ public class ExceptionHandlingMiddleware
|
|||||||
message = notFoundEx.Message;
|
message = notFoundEx.Message;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ForbiddenException forbiddenEx:
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
|
||||||
message = forbiddenEx.Message;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.LogError(exception, "Unhandled exception occurred.");
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
@@ -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 record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||||
{
|
{
|
||||||
public bool HasAccessCode => AccessCode is not null;
|
public bool HasAccessCode => AccessCode is not null;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace EnvelopeGenerator.Server.Models;
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the keys and default values used for authentication token handling
|
/// Represents the keys and default values used for authentication token handling
|
||||||
/// within the Envelope Generator Server.
|
/// within the Envelope Generator API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AuthTokenKeys
|
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>
|
/// <summary>
|
||||||
/// Represents a hyperlink for contact purposes with various HTML attributes.
|
/// Represents a hyperlink for contact purposes with various HTML attributes.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Models;
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
public class Culture
|
public class Culture
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Server.Models;
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
public class Cultures : List<Culture>
|
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
|
public class ErrorViewModel
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Server.Models;
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
public class Image
|
public class Image
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Models;
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.
|
/// 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
|
public class MainViewModel
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
public record Annotation : IAnnotation
|
public record Annotation : IAnnotation
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
public class AnnotationParams
|
public class AnnotationParams
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
|
/// 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
|
public record Color
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
public interface IAnnotation
|
public interface IAnnotation
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Server.Models;
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the parameters for two-factor authentication (2FA) registration.
|
/// 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,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:5131",
|
"applicationUrl": "http://localhost:4176",
|
||||||
"sslPort": 8088
|
"sslPort": 44300
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"https (Blazor UI)": {
|
"http": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
}
|
||||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
|
|
||||||
},
|
},
|
||||||
"https (Swagger API)": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"http (Development)": {
|
"angular": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "http://localhost:5131",
|
"launchUrl": "",
|
||||||
|
"applicationUrl": "https://localhost:7174;http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AuthClientParams": {
|
"AuthClientParams": {
|
||||||
"Url": "http://172.24.12.39:9090/auth-hub",
|
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||||
"PublicKeys": [
|
"PublicKeys": [
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"UseSwagger": true,
|
"UseSwagger": true,
|
||||||
"UseScalar": true,
|
"UseDbMigration": true,
|
||||||
"UseDbMigration": false,
|
|
||||||
"DiPMode": true,
|
"DiPMode": true,
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
@@ -10,15 +9,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"AllowedOrigins": [
|
"AllowedOrigins": [ "http://localhost:4200", "http://172.24.12.39:9090", "https://localhost:8088", "http://localhost:5131", "http://localhost:7192" ],
|
||||||
"http://localhost:4200",
|
|
||||||
"http://172.24.12.39:9090",
|
|
||||||
"https://localhost:8088",
|
|
||||||
"http://localhost:5131",
|
|
||||||
"http://localhost:7192"
|
|
||||||
],
|
|
||||||
"ConnectionStrings": {
|
"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;"
|
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||||
},
|
},
|
||||||
"DirectorySearchOptions": {
|
"DirectorySearchOptions": {
|
||||||
@@ -46,15 +39,6 @@
|
|||||||
"Issuer": "auth.digitaldata.works",
|
"Issuer": "auth.digitaldata.works",
|
||||||
"Audience": "sign-flow.digitaldata.works"
|
"Audience": "sign-flow.digitaldata.works"
|
||||||
},
|
},
|
||||||
"ApiOptions": {
|
|
||||||
"BaseUrl": ""
|
|
||||||
},
|
|
||||||
"PdfViewerOptions": {
|
|
||||||
"ThumbnailBaseScale": 0.75,
|
|
||||||
"ThumbnailEnableHiDPI": true,
|
|
||||||
"MainCanvasEnableHiDPI": true,
|
|
||||||
"ZoomStepPercentage": 5
|
|
||||||
},
|
|
||||||
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
|
"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.
|
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
@@ -71,7 +55,7 @@
|
|||||||
"throwConfigExceptions": true,
|
"throwConfigExceptions": true,
|
||||||
"variables": {
|
"variables": {
|
||||||
"logDirectory": "E:\\LogFiles\\Digital Data\\signFlow",
|
"logDirectory": "E:\\LogFiles\\Digital Data\\signFlow",
|
||||||
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Server"
|
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Web"
|
||||||
},
|
},
|
||||||
"targets": {
|
"targets": {
|
||||||
"infoLogs": {
|
"infoLogs": {
|
||||||
@@ -146,17 +130,15 @@
|
|||||||
"CustomImages": {
|
"CustomImages": {
|
||||||
"App": {
|
"App": {
|
||||||
"Src": "/img/DD_signFLOW_LOGO.png",
|
"Src": "/img/DD_signFLOW_LOGO.png",
|
||||||
"Height": "28px",
|
|
||||||
"Classes": {
|
"Classes": {
|
||||||
"Main": "signflow-app-logo"
|
"Main": "signFlow-logo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Company": {
|
"Company": {
|
||||||
"Src": "/img/digital_data.svg",
|
"Src": "/img/digital_data.svg",
|
||||||
"Height": "33px",
|
|
||||||
"Classes": {
|
"Classes": {
|
||||||
"Show": "signflow-company-logo",
|
"Show": "dd-show-logo",
|
||||||
"Locked": "signflow-company-logo"
|
"Locked": "dd-locked-logo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -186,21 +168,12 @@
|
|||||||
},
|
},
|
||||||
"DbTriggerParams": {
|
"DbTriggerParams": {
|
||||||
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
"History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
"EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
"DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ],
|
|
||||||
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
||||||
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
||||||
"Receiver": [],
|
"Receiver": [],
|
||||||
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
||||||
},
|
},
|
||||||
"Cache": {
|
|
||||||
"SignatureCacheExpiration": null,
|
|
||||||
"SqlServer": {
|
|
||||||
"ConnectionString": null,
|
|
||||||
"SchemaName": "dbo",
|
|
||||||
"TableName": "TBDD_CACHE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MainPageTitle": null,
|
"MainPageTitle": null,
|
||||||
"AnnotationParams": {
|
"AnnotationParams": {
|
||||||
"Background": {
|
"Background": {
|
||||||
@@ -10,20 +10,6 @@
|
|||||||
"Transforms": [
|
"Transforms": [
|
||||||
{ "PathSet": "/api/auth/sign-flow" }
|
{ "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": {
|
"Clusters": {
|
||||||
@@ -36,4 +22,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,77 +8,42 @@ public record AnnotationCreateDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long Id { get; set; }
|
public int ElementId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Not required for DevExpress")]
|
public string Name { get; init; } = null!;
|
||||||
public int ElementId { get; init; } = -1;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Not required for DevExpress")]
|
public string Value { get; init; } = null!;
|
||||||
public string Name { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Not required for DevExpress")]
|
public string Type { get; init; } = null!;
|
||||||
public string Value { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </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; }
|
public double? X { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public double? Y { get; init; }
|
public double? Y { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public double? Width { get; init; }
|
public double? Width { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public double? Height { get; init; }
|
public double? Height { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Added to eliminate the need for SignatureDto in DevExpress
|
|
||||||
/// </summary>
|
|
||||||
public int? Page { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing configuration settings.
|
/// Data Transfer Object representing configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class ConfigDto
|
public class ConfigDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class DocumentDto
|
public class DocumentDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -28,5 +31,5 @@ public class DocumentDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
|
public IEnumerable<SignatureDto>? Elements { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class DocumentStatusDto
|
public class DocumentStatusDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using DigitalData.UserManager.Application.DTOs.User;
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeDto : IEnvelope
|
public record EnvelopeDto : IEnvelope
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -125,14 +126,4 @@ public record EnvelopeDto : IEnvelope
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<DocumentDto>? Documents { get; set; }
|
public IEnumerable<DocumentDto>? Documents { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<HistoryDto>? Histories { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeReceiverDto
|
public record EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -60,11 +61,6 @@ public record EnvelopeReceiverDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PrivateMessage { get; init; }
|
public string? PrivateMessage { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string? AccessCode { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -79,13 +75,4 @@ public record EnvelopeReceiverDto
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPhoneNumber { get; init; }
|
public bool HasPhoneNumber { get; init; }
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether this receiver has signed the envelope.
|
|
||||||
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
|
|
||||||
/// </summary>
|
|
||||||
public bool Signed => Envelope?.Histories?.Any(h =>
|
|
||||||
h.Receiver?.Id == ReceiverId &&
|
|
||||||
h.Status == EnvelopeStatus.DocumentSigned
|
|
||||||
) ?? false;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="DateValid"></param>
|
/// <param name="DateValid"></param>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||||
DateTime DateValid)
|
DateTime DateValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,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;
|
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.
|
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||||
/// Contains information about the receiver, associated envelope, and audit details.
|
/// Contains information about the receiver, associated envelope, and audit details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class EnvelopeReceiverReadOnlyDto
|
public class EnvelopeReceiverReadOnlyDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class EnvelopeTypeDto
|
public class EnvelopeTypeDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the AutoMapper profile configuration for mapping between
|
/// Represents the AutoMapper profile configuration for mapping between
|
||||||
@@ -25,39 +23,34 @@ public class MappingProfile : Profile
|
|||||||
{
|
{
|
||||||
// Entity to DTO mappings
|
// Entity to DTO mappings
|
||||||
CreateMap<Config, ConfigDto>();
|
CreateMap<Config, ConfigDto>();
|
||||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
CreateMap<Signature, SignatureDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>()
|
CreateMap<Envelope, EnvelopeDto>();
|
||||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
|
||||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<Domain.Entities.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<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||||
CreateMap<Receiver, ReceiverDto>();
|
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||||
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||||
|
|
||||||
// DTO to Entity mappings
|
// DTO to Entity mappings
|
||||||
CreateMap<ConfigDto, Config>();
|
CreateMap<ConfigDto, Config>();
|
||||||
CreateMap<DocReceiverElementDto, DocReceiverElement>();
|
CreateMap<SignatureDto, Signature>();
|
||||||
CreateMap<Signature, DocReceiverElement>()
|
|
||||||
.ForMember(dest => dest.Ink, opt => opt.MapFrom(src => src.DataUrl.MapDataUrlToRequiredBytes()))
|
|
||||||
.MapChangedWhen();
|
|
||||||
CreateMap<DocumentStatusDto, DocumentStatus>();
|
CreateMap<DocumentStatusDto, DocumentStatus>();
|
||||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||||
CreateMap<EnvelopeDto, Envelope>();
|
CreateMap<EnvelopeDto, Envelope>();
|
||||||
CreateMap<DocumentDto, Document>();
|
CreateMap<DocumentDto, Document>();
|
||||||
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
||||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||||
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||||
.MapAddedWhen();
|
.MapAddedWhen();
|
||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record SmsResponse
|
public record SmsResponse
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -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 EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
@@ -6,6 +7,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class ReceiverDto
|
public class ReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
|
||||||
/// This model holds the signature image (as base64 data URL) along with signer information
|
|
||||||
/// used for rendering applied signatures on the PDF canvas.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
|
|
||||||
/// </remarks>
|
|
||||||
public sealed record Signature
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
|
|
||||||
/// </summary>
|
|
||||||
public required int Id { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base64-encoded data URL of the signature image.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
|
|
||||||
/// </summary>
|
|
||||||
public required string DataUrl { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Full name of the signer (first and last name).
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Required:</b> Yes (validated in popup)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Example:</b> "Max Mustermann"
|
|
||||||
/// </summary>
|
|
||||||
public required string FullName { get; init; }
|
|
||||||
|
|
||||||
private readonly string? _position = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Job title or position of the signer.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Required:</b> No (optional field)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Example:</b> "Geschäftsführer" or empty string
|
|
||||||
/// </summary>
|
|
||||||
public string? Position
|
|
||||||
{
|
|
||||||
get => _position;
|
|
||||||
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Location/place where the signature was created.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Required:</b> Yes (validated in popup)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
|
||||||
/// </summary>
|
|
||||||
public required string Place { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocReceiverElementDto : IDocReceiverElement
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
public class SignatureDto : ISignature
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the element.
|
/// Gets or sets the unique identifier of the element.
|
||||||
@@ -92,34 +93,4 @@ public class DocReceiverElementDto : IDocReceiverElement
|
|||||||
/// Gets or sets the left position of the element (in layout terms).
|
/// Gets or sets the left position of the element (in layout terms).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Left => X;
|
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; }
|
|
||||||
}
|
}
|
||||||
@@ -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,6 +1,5 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
namespace EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
|
||||||
@@ -14,29 +13,12 @@ public static class AutoMapperAuditingExtensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static IMappingExpression<TSource, TDestination> MapAddedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
public static IMappingExpression<TSource, TDestination> MapAddedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
||||||
where TDestination : IHasAddedWhen
|
where TDestination : IHasAddedWhen
|
||||||
=> expression.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
=> expression.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps <see cref="IHasChangedWhen.ChangedWhen"/> to the current UTC time.
|
/// Maps <see cref="IHasChangedWhen.ChangedWhen"/> to the current UTC time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
||||||
where TDestination : IHasChangedWhen
|
where TDestination : IHasChangedWhen
|
||||||
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a base64 data URL string to a byte array.
|
|
||||||
/// Handles data URLs in the format: "data:image/png;base64,iVBORw0KG..."
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dataUrl">The base64 data URL string from Canvas.toDataURL()</param>
|
|
||||||
/// <returns>The decoded byte array, or null if the input is null or empty</returns>
|
|
||||||
public static byte[]? MapDataUrlToRequiredBytes(this string dataUrl)
|
|
||||||
{
|
|
||||||
// Remove data URL prefix (e.g., "data:image/png;base64,")
|
|
||||||
var base64Index = dataUrl.IndexOf(',', StringComparison.Ordinal);
|
|
||||||
if (base64Index == -1)
|
|
||||||
throw new ArgumentException("Invalid data URL format. Unable to extract base64 data.", nameof(dataUrl));
|
|
||||||
|
|
||||||
var base64Data = dataUrl[(base64Index + 1)..];
|
|
||||||
return Convert.FromBase64String(base64Data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use IRepository")]
|
[Obsolete("Use IRepository")]
|
||||||
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int>
|
public interface IDocumentReceiverElementRepository : ICRUDRepository<Signature, int>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use MediatR")]
|
[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>
|
|
||||||
/// 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,37 +1,88 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using System.Dynamic;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notification raised when a document is signed by a receiver.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
/// <param name="Instant"></param>
|
||||||
public record DocSignedNotification : INotification, ISendMailNotification
|
/// <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>
|
/// <summary>
|
||||||
/// The envelope receiver information.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required EnvelopeReceiverDto EnvelopeReceiver { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The PSPDFKit annotation data.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
|
||||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the email template type.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the email address of the receiver.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
public string EmailAddress => Receiver?.EmailAddress
|
||||||
?? throw new InvalidOperationException($"Receiver is null." +
|
?? throw new InvalidOperationException($"Receiver is null." +
|
||||||
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static class DocSignedNotificationExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The DTO to convert.</param>
|
||||||
|
/// <param name="psPdfKitAnnotation"></param>
|
||||||
|
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
||||||
|
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
|
||||||
|
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dtoTask"></param>
|
||||||
|
/// <param name="psPdfKitAnnotation"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
|
||||||
|
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="publisher"></param>
|
||||||
|
/// <param name="notification"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await publisher.Publish(notification, cancel);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await publisher.Publish(new RemoveSignatureNotification()
|
||||||
|
{
|
||||||
|
EnvelopeId = notification.EnvelopeId,
|
||||||
|
ReceiverId = notification.ReceiverId
|
||||||
|
}, cancel);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
|
||||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -9,7 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
|
||||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||||
{
|
{
|
||||||
private const string BlankAnnotationJson = "{}";
|
private const string BlankAnnotationJson = "{}";
|
||||||
@@ -31,11 +29,10 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
|||||||
/// <param name="notification"></param>
|
/// <param name="notification"></param>
|
||||||
/// <param name="cancel"></param>
|
/// <param name="cancel"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
|
||||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
||||||
{
|
{
|
||||||
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
EnvelopeId = notification.EnvelopeId,
|
||||||
ReceiverId = notification.EnvelopeReceiver.ReceiverId,
|
ReceiverId = notification.ReceiverId,
|
||||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||||
: BlankAnnotationJson
|
: BlankAnnotationJson
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (notification.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)}");
|
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
await _sender.Send(new CreateHistoryCommand()
|
await _sender.Send(new CreateHistoryCommand()
|
||||||
{
|
{
|
||||||
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
EnvelopeId = notification.EnvelopeId,
|
||||||
UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress,
|
UserReference = notification.Receiver.EmailAddress,
|
||||||
Status = EnvelopeStatus.DocumentSigned,
|
Status = EnvelopeStatus.DocumentSigned,
|
||||||
}, cancel);
|
}, cancel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
|||||||
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
||||||
{
|
{
|
||||||
emailOut.ReferenceString = notification.EmailAddress;
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
emailOut.ReferenceId = notification.ReceiverId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -42,11 +42,11 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
|||||||
{
|
{
|
||||||
var placeHolders = new Dictionary<string, string>()
|
var placeHolders = new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
{ "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? string.Empty },
|
{ "[NAME_RECEIVER]", notification.Name ?? string.Empty },
|
||||||
{ "[DOCUMENT_TITLE]", notification.EnvelopeReceiver.Envelope?.Title ?? 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["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
||||||
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -113,7 +113,7 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
|||||||
EmailAddress = notification.EmailAddress,
|
EmailAddress = notification.EmailAddress,
|
||||||
EmailBody = temp.Body,
|
EmailBody = temp.Body,
|
||||||
EmailSubj = temp.Subject,
|
EmailSubj = temp.Subject,
|
||||||
AddedWhen = DateTime.Now,
|
AddedWhen = DateTime.UtcNow,
|
||||||
AddedWho = DispatcherParams.AddedWho,
|
AddedWho = DispatcherParams.AddedWho,
|
||||||
SendingProfile = DispatcherParams.SendingProfile,
|
SendingProfile = DispatcherParams.SendingProfile,
|
||||||
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
namespace EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Query;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
@@ -31,6 +29,5 @@ public record ReceiverQueryBase
|
|||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
||||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[NotMapped]
|
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||||
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
|
|||||||
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Raw => @"
|
public string Raw => @"
|
||||||
DECLARE @OUT_RECEIVER_ID bigint
|
DECLARE @OUT_RECEIVER_ID int
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||||
{0},
|
{0},
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using QRCoder;
|
using QRCoder;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using MediatR;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application;
|
namespace EnvelopeGenerator.Application;
|
||||||
|
|
||||||
@@ -51,30 +48,14 @@ public static class DependencyInjection
|
|||||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||||
|
|
||||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||||
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
||||||
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
|
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
|
||||||
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
||||||
services.TryAddSingleton<QRCodeGenerator>();
|
services.TryAddSingleton<QRCodeGenerator>();
|
||||||
|
|
||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
// Register SignCommand pipeline behaviors in execution order
|
|
||||||
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
|
||||||
|
|
||||||
// 1. AnnotationBehavior - Saves annotations (executes second)
|
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
|
||||||
|
|
||||||
// 2. DocStatusBehavior - Creates document status (executes third)
|
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
|
||||||
|
|
||||||
// 3. HistoryBehavior - Records history (executes fourth)
|
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
|
||||||
|
|
||||||
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pipeline behavior that saves annotations.
|
|
||||||
/// Executes first in the signing process.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("The PSPDFKit library is deprecated.")]
|
|
||||||
public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IRepository<ElementAnnotation> _repo;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="AnnotationBehavior"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public AnnotationBehavior(IRepository<ElementAnnotation> repository)
|
|
||||||
{
|
|
||||||
_repo = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="next"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
if(request.ReceiverAppType != ReceiverAppType.LegacyWeb)
|
|
||||||
if(request.PsPdfKitAnnotation is null)
|
|
||||||
return await next(cancel);
|
|
||||||
else
|
|
||||||
throw new BadRequestException("PsPdfKit Annotation are only supported for the legacy web receiver type.");
|
|
||||||
|
|
||||||
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
|
|
||||||
await _repo.CreateAsync(annot.Structured, cancel);
|
|
||||||
else
|
|
||||||
throw new BadRequestException("Annotation data is missing or invalid.");
|
|
||||||
|
|
||||||
return await next(cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pipeline behavior that creates document status.
|
|
||||||
/// Executes second in the signing process.
|
|
||||||
/// </summary>
|
|
||||||
public class DocStatusBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|
||||||
{
|
|
||||||
private const string BlankAnnotationJson = "{}";
|
|
||||||
|
|
||||||
private readonly ISender _sender;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender"></param>
|
|
||||||
public DocStatusBehavior(ISender sender)
|
|
||||||
{
|
|
||||||
_sender = sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="next"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
|
||||||
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await _sender.Send(new CreateDocStatusCommand()
|
|
||||||
{
|
|
||||||
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
|
|
||||||
ReceiverId = request.EnvelopeReceiver.ReceiverId,
|
|
||||||
Value = request.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
|
||||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
|
||||||
: BlankAnnotationJson
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
return await next(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pipeline behavior that resolves and validates EnvelopeReceiver.
|
|
||||||
/// Executes FIRST in the signing process - before all other behaviors.
|
|
||||||
/// If EnvelopeReceiver is not provided, it queries the database using EnvelopeReceiverQueryBase parameters.
|
|
||||||
/// </summary>
|
|
||||||
public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IRepository<EnvelopeReceiver> _erRepo;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="erRepo"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public EnvelopeReceiverResolutionBehavior(IRepository<EnvelopeReceiver> erRepo, IMapper mapper)
|
|
||||||
{
|
|
||||||
_erRepo = erRepo;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="next"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// If EnvelopeReceiver is not provided, query it from database
|
|
||||||
if (request.EnvelopeReceiver is null)
|
|
||||||
{
|
|
||||||
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
|
||||||
?? throw new NotFoundException("EnvelopeReceiver not found");
|
|
||||||
|
|
||||||
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await next(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Histories.Commands;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pipeline behavior that records history.
|
|
||||||
/// Executes third in the signing process.
|
|
||||||
/// </summary>
|
|
||||||
public class HistoryBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly ISender _sender;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender"></param>
|
|
||||||
public HistoryBehavior(ISender sender)
|
|
||||||
{
|
|
||||||
_sender = sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="next"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (request.EnvelopeReceiver.Receiver is null)
|
|
||||||
throw new InvalidOperationException($"Receiver information is missing in the notification. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
|
|
||||||
|
|
||||||
await _sender.Send(new CreateHistoryCommand()
|
|
||||||
{
|
|
||||||
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
|
|
||||||
UserReference = request.EnvelopeReceiver.Receiver.EmailAddress,
|
|
||||||
Status = EnvelopeStatus.DocumentSigned,
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
return await next(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pipeline behavior that creates document status.
|
|
||||||
/// Executes second in the signing process.
|
|
||||||
/// </summary>
|
|
||||||
public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly ISender _sender;
|
|
||||||
|
|
||||||
private readonly IRepository<DocReceiverElement> _elementRepo;
|
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender"></param>
|
|
||||||
/// <param name="elementRepo"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public SaveSignatureBehavior(ISender sender, IRepository<DocReceiverElement> elementRepo, IMapper mapper)
|
|
||||||
{
|
|
||||||
_sender = sender;
|
|
||||||
_elementRepo = elementRepo;
|
|
||||||
_elementRepo = elementRepo;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="next"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
if (request.ReceiverAppType == ReceiverAppType.LegacyWeb)
|
|
||||||
return await next(cancel);
|
|
||||||
else if(request.Signatures is not IEnumerable<Signature> signatures)
|
|
||||||
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
|
||||||
|
|
||||||
var elements = await _elementRepo
|
|
||||||
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
|
||||||
.Where(e => e.ReceiverId == request.Receiver.Id)
|
|
||||||
.ToListAsync(cancel);
|
|
||||||
|
|
||||||
foreach (var element in elements)
|
|
||||||
{
|
|
||||||
var signatures = request.Signatures.Where(s => s.Id == element.Id).ToList();
|
|
||||||
if(signatures.Count == 0)
|
|
||||||
throw new BadRequestException("No signature found for element with id {element.Id}.");
|
|
||||||
else if(signatures.Count > 1)
|
|
||||||
throw new BadRequestException("Multiple signatures found for element with id {element.Id}.");
|
|
||||||
|
|
||||||
await _elementRepo.UpdateAsync(signatures.First(), e => e.Id == element.Id, cancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await next(cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pipeline behavior that sends signed mail notification.
|
|
||||||
/// Executes LAST in the signing process - only if all previous behaviors succeed.
|
|
||||||
/// </summary>
|
|
||||||
public class SendSignedMailBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IRepository<EmailTemplate> _tempRepo;
|
|
||||||
private readonly IRepository<EmailOut> _emailOutRepo;
|
|
||||||
private readonly MailParams _mailParams;
|
|
||||||
private readonly DispatcherParams _dispatcherParams;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="tempRepo"></param>
|
|
||||||
/// <param name="emailOutRepo"></param>
|
|
||||||
/// <param name="mailParamsOptions"></param>
|
|
||||||
/// <param name="dispatcherParamsOptions"></param>
|
|
||||||
public SendSignedMailBehavior(
|
|
||||||
IRepository<EmailTemplate> tempRepo,
|
|
||||||
IRepository<EmailOut> emailOutRepo,
|
|
||||||
IOptions<MailParams> mailParamsOptions,
|
|
||||||
IOptions<DispatcherParams> dispatcherParamsOptions)
|
|
||||||
{
|
|
||||||
_tempRepo = tempRepo;
|
|
||||||
_emailOutRepo = emailOutRepo;
|
|
||||||
_mailParams = mailParamsOptions.Value;
|
|
||||||
_dispatcherParams = dispatcherParamsOptions.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="next"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var placeHolders = CreatePlaceHolders(request);
|
|
||||||
|
|
||||||
var temp = await _tempRepo
|
|
||||||
.Where(x => x.Name == EmailTemplateType.DocumentSigned.ToString())
|
|
||||||
.SingleOrDefaultAsync(cancellationToken)
|
|
||||||
?? throw new InvalidOperationException($"Email template not found. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
|
|
||||||
|
|
||||||
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, _mailParams.Placeholders);
|
|
||||||
temp.Body = ReplacePlaceHolders(temp.Body, placeHolders, _mailParams.Placeholders);
|
|
||||||
|
|
||||||
var emailOut = new EmailOut
|
|
||||||
{
|
|
||||||
EmailAddress = request.EnvelopeReceiver.Receiver!.EmailAddress,
|
|
||||||
EmailBody = temp.Body,
|
|
||||||
EmailSubj = temp.Subject,
|
|
||||||
AddedWhen = DateTime.Now,
|
|
||||||
AddedWho = _dispatcherParams.AddedWho,
|
|
||||||
SendingProfile = _dispatcherParams.SendingProfile,
|
|
||||||
ReminderTypeId = _dispatcherParams.ReminderTypeId,
|
|
||||||
EmailAttmt1 = _dispatcherParams.EmailAttmt1,
|
|
||||||
WfId = (int)EnvelopeStatus.MessageConfirmationSent,
|
|
||||||
ReferenceString = request.EnvelopeReceiver.Receiver!.EmailAddress,
|
|
||||||
ReferenceId = request.EnvelopeReceiver.ReceiverId
|
|
||||||
};
|
|
||||||
|
|
||||||
await _emailOutRepo.CreateAsync(emailOut, cancellationToken);
|
|
||||||
|
|
||||||
return await next(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> CreatePlaceHolders(SigningCommand request)
|
|
||||||
{
|
|
||||||
var placeHolders = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "[NAME_RECEIVER]", request.EnvelopeReceiver.Name ?? string.Empty },
|
|
||||||
{ "[DOCUMENT_TITLE]", request.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.EnvelopeReceiver.Envelope.IsReadAndConfirm())
|
|
||||||
{
|
|
||||||
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
|
||||||
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
|
||||||
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
|
|
||||||
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
|
|
||||||
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
|
|
||||||
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
|
|
||||||
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
|
|
||||||
placeHolders["[FINAL_STATUS]"] = "Signatur";
|
|
||||||
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
|
|
||||||
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
|
|
||||||
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
|
|
||||||
}
|
|
||||||
|
|
||||||
return placeHolders;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReplacePlaceHolders(string text, params Dictionary<string, string>[] placeHoldersList)
|
|
||||||
{
|
|
||||||
foreach (var placeHolders in placeHoldersList)
|
|
||||||
foreach (var ph in placeHolders)
|
|
||||||
text = text.Replace(ph.Key, ph.Value);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command to sign a document by a receiver.
|
|
||||||
/// </summary>
|
|
||||||
public record SigningCommand : EnvelopeReceiverQueryBase, IRequest
|
|
||||||
{
|
|
||||||
private EnvelopeReceiverDto? _envelopeReceiver;
|
|
||||||
|
|
||||||
internal void SetEnvelopeReceiver(EnvelopeReceiverDto envelopeReceiver)
|
|
||||||
{
|
|
||||||
_envelopeReceiver = envelopeReceiver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The envelope receiver information.
|
|
||||||
/// </summary>
|
|
||||||
public EnvelopeReceiverDto EnvelopeReceiver
|
|
||||||
{
|
|
||||||
get => _envelopeReceiver!;
|
|
||||||
init => _envelopeReceiver = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The PSPDFKit annotation data.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
|
||||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<Signature>? Signatures { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public ReceiverAppType ReceiverAppType { get; init; } = ReceiverAppType.ReceiverUI;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles the sign command. All work is done by pipeline behaviors.
|
|
||||||
/// This handler is intentionally empty - behaviors handle all the processing.
|
|
||||||
/// </summary>
|
|
||||||
public class SignCommandHandler : IRequestHandler<SigningCommand>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Executes the signing command. Pipeline behaviors handle all processing.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task Handle(SigningCommand request, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public enum ReceiverAppType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
ReceiverUI = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
LegacyWeb = 1,
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceiverElementQuery, IEnumerable<DocReceiverElementDto>>
|
|
||||||
{
|
|
||||||
private readonly IRepository<DocReceiverElement> _repository;
|
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public ReadDocReceiverElementQueryHandler(IRepository<DocReceiverElement> repository, IMapper mapper)
|
|
||||||
{
|
|
||||||
_repository = repository;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="NotImplementedException"></exception>
|
|
||||||
public async Task<IEnumerable<DocReceiverElementDto>> Handle(ReadDocReceiverElementQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var q = _repository.Query;
|
|
||||||
|
|
||||||
if(request.Envelope.Id is int envelopeId)
|
|
||||||
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
|
|
||||||
|
|
||||||
if (request.Envelope.Uuid is string envelopeUuid)
|
|
||||||
q = q.Where(e => e.Document.Envelope.Uuid == envelopeUuid);
|
|
||||||
|
|
||||||
if (request.Receiver.Id is int receiverId)
|
|
||||||
q = q.Where(e => e.ReceiverId == receiverId);
|
|
||||||
|
|
||||||
if (request.Receiver.Signature is string signature)
|
|
||||||
q = q.Where(e => e.Receiver.Signature == signature);
|
|
||||||
|
|
||||||
var elements = await q.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<UpdateDocStatusCommand, DocumentStatus>()
|
CreateMap<UpdateDocStatusCommand, DocumentStatus>()
|
||||||
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
|
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.StatusChangedWhen, opt => opt.MapFrom(src => DateTime.Now))
|
.ForMember(dest => dest.StatusChangedWhen, opt => opt.MapFrom(src => DateTime.UtcNow))
|
||||||
.MapChangedWhen();
|
.MapChangedWhen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,7 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
||||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
||||||
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
|
||||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,20 +53,14 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
|||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
|
|
||||||
|
|
||||||
if (query.UserId is int userId)
|
|
||||||
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
|
||||||
|
|
||||||
if (query.Id is not null)
|
if (query.Id is not null)
|
||||||
{
|
{
|
||||||
var doc = await docQuery.Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
|
var doc = await _repo.Query.Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
|
||||||
|
|
||||||
return _mapper.Map<DocumentDto>(doc);
|
return _mapper.Map<DocumentDto>(doc);
|
||||||
}
|
}
|
||||||
else if (query.EnvelopeId is not null)
|
else if (query.EnvelopeId is not null)
|
||||||
{
|
{
|
||||||
var doc = await docQuery.Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
|
var doc = await _repo.Query.Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
|
||||||
return _mapper.Map<DocumentDto>(doc);
|
return _mapper.Map<DocumentDto>(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Resources\Model.Designer.vb" />
|
<None Remove="Resources\Model.Designer.vb" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
@@ -78,25 +79,25 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>7.0.5</Version>
|
<Version>7.0.5</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public record CreateEnvelopeReceiverCommand : CreateEnvelopeCommand, IRequest<Cr
|
|||||||
/// <param name="X">X-Position</param>
|
/// <param name="X">X-Position</param>
|
||||||
/// <param name="Y">Y-Position</param>
|
/// <param name="Y">Y-Position</param>
|
||||||
/// <param name="Page">Seite, auf der sie sich befindet</param>
|
/// <param name="Page">Seite, auf der sie sich befindet</param>
|
||||||
public record DocReceiverElementCreateDto([Required] double X, [Required] double Y, [Required] int Page);
|
public record Signature([Required] double X, [Required] double Y, [Required] int Page);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
|
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
|
||||||
@@ -41,7 +41,7 @@ public class ReceiverGetOrCreateCommand
|
|||||||
/// Unterschriften auf Dokumenten.
|
/// Unterschriften auf Dokumenten.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public List<DocReceiverElementCreateDto> DocReceiverElements { get; init; } = new();
|
public List<Signature> Signatures { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Name, mit dem der Empfänger angesprochen werden soll.
|
/// Der Name, mit dem der Empfänger angesprochen werden soll.
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
|
||||||
using EnvelopeGenerator.Application.Receivers.Queries;
|
|
||||||
using MediatR;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a query for reading an envelope receiver including sensitive fields
|
|
||||||
/// (access code, phone number) that are excluded from the standard <see cref="ReadEnvelopeReceiverQuery"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Returns a single <see cref="EnvelopeReceiverSecretDto"/> matched by UUID and receiver signature.
|
|
||||||
/// Equivalent to the legacy <c>ReadWithSecretByUuidSignatureAsync</c> service method.
|
|
||||||
/// </remarks>
|
|
||||||
public record ReadEnvelopeReceiverSecretQuery
|
|
||||||
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
|
|
||||||
IRequest<EnvelopeReceiverSecretDto?>;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static class ReadEnvelopeReceiverSecretQueryExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a <see cref="ReadEnvelopeReceiverSecretQuery"/> using the composite key (uuid::signature).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mediator">The mediator instance.</param>
|
|
||||||
/// <param name="key">Composite key in the format <c>uuid::signature</c>.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
/// <returns>The matching <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if not found.</returns>
|
|
||||||
public static Task<EnvelopeReceiverSecretDto?> ReadEnvelopeReceiverSecretAsync(
|
|
||||||
this IMediator mediator,
|
|
||||||
string key,
|
|
||||||
CancellationToken cancel = default)
|
|
||||||
=> mediator.Send(new ReadEnvelopeReceiverSecretQuery { Key = key }, cancel);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a <see cref="ReadEnvelopeReceiverSecretQuery"/> using UUID and receiver signature.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mediator">The mediator instance.</param>
|
|
||||||
/// <param name="uuid">Envelope UUID.</param>
|
|
||||||
/// <param name="signature">Receiver signature.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
/// <returns>The matching <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if not found.</returns>
|
|
||||||
public static Task<EnvelopeReceiverSecretDto?> ReadEnvelopeReceiverSecretAsync(
|
|
||||||
this IMediator mediator,
|
|
||||||
string uuid,
|
|
||||||
string signature,
|
|
||||||
CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var q = new ReadEnvelopeReceiverSecretQuery();
|
|
||||||
q.Envelope.Uuid = uuid;
|
|
||||||
q.Receiver.Signature = signature;
|
|
||||||
return mediator.Send(q, cancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles <see cref="ReadEnvelopeReceiverSecretQuery"/> and returns a
|
|
||||||
/// <see cref="EnvelopeReceiverSecretDto"/> containing sensitive fields.
|
|
||||||
/// </summary>
|
|
||||||
public class ReadEnvelopeReceiverSecretQueryHandler
|
|
||||||
: IRequestHandler<ReadEnvelopeReceiverSecretQuery, EnvelopeReceiverSecretDto?>
|
|
||||||
{
|
|
||||||
private readonly IRepository<EnvelopeReceiver> _repo;
|
|
||||||
private readonly IRepository<Receiver> _rcvRepo;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="ReadEnvelopeReceiverSecretQueryHandler"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeReceiver">Repository for <see cref="EnvelopeReceiver"/>.</param>
|
|
||||||
/// <param name="rcvRepo">Repository for <see cref="Receiver"/>.</param>
|
|
||||||
/// <param name="mapper">AutoMapper instance.</param>
|
|
||||||
public ReadEnvelopeReceiverSecretQueryHandler(
|
|
||||||
IRepository<EnvelopeReceiver> envelopeReceiver,
|
|
||||||
IRepository<Receiver> rcvRepo,
|
|
||||||
IMapper mapper)
|
|
||||||
{
|
|
||||||
_repo = envelopeReceiver;
|
|
||||||
_rcvRepo = rcvRepo;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles the query and returns the matching <see cref="EnvelopeReceiverSecretDto"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The query containing filter criteria.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The matched <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if no record is found.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<EnvelopeReceiverSecretDto?> Handle(
|
|
||||||
ReadEnvelopeReceiverSecretQuery request,
|
|
||||||
CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var q = _repo.Query.Where(request, notnull: false);
|
|
||||||
|
|
||||||
var envRcvs = await q
|
|
||||||
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
|
|
||||||
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
|
|
||||||
.Include(er => er.Envelope).ThenInclude(e => e!.User)
|
|
||||||
.Include(er => er.Receiver)
|
|
||||||
.ToListAsync(cancel);
|
|
||||||
|
|
||||||
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0)
|
|
||||||
{
|
|
||||||
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel);
|
|
||||||
|
|
||||||
foreach (var item in envRcvs)
|
|
||||||
item.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
var envRcv = envRcvs.FirstOrDefault();
|
|
||||||
if (envRcv is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return _mapper.Map<EnvelopeReceiverSecretDto>(envRcv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
@@ -12,29 +13,23 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||||
{
|
{
|
||||||
public int? EnvelopeId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Title { get; set; }
|
[Required]
|
||||||
|
public required string Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Message { get; set; }
|
[Required]
|
||||||
|
public required string Message { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TFAEnabled { get; set; } = false;
|
public bool TFAEnabled { get; set; } = false;
|
||||||
|
|
||||||
public bool Send { get; init; }
|
|
||||||
|
|
||||||
public CreateEnvelopeDocumentDto? Document { get; init; }
|
|
||||||
|
|
||||||
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID des Absenders
|
/// ID des Absenders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -45,10 +40,10 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public CreateEnvelopeCommand WithAuth(int userId)
|
public bool Authorize(int userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
return this;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -60,14 +55,4 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool UseSQLExecutor { get; set; } = true;
|
public bool UseSQLExecutor { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
|
||||||
|
|
||||||
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
|
|
||||||
|
|
||||||
public record CreateEnvelopeReceiverDto(
|
|
||||||
string EmailAddress,
|
|
||||||
string? Name,
|
|
||||||
string? PhoneNumber,
|
|
||||||
List<CreateEnvelopeFieldDto> Fields);
|
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
using EnvelopeGenerator.Application.Histories.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
@@ -29,26 +19,6 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
|
|
||||||
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
||||||
|
|
||||||
private IRepository<Document> DocumentRepository => _provider.GetRequiredService<IRepository<Document>>();
|
|
||||||
|
|
||||||
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => _provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
|
||||||
|
|
||||||
private IRepository<Receiver> ReceiverRepository => _provider.GetRequiredService<IRepository<Receiver>>();
|
|
||||||
|
|
||||||
private IRepository<DocReceiverElement> DocReceiverElementRepository => _provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
|
||||||
|
|
||||||
private IRepository<EmailTemplate> EmailTemplateRepository => _provider.GetRequiredService<IRepository<EmailTemplate>>();
|
|
||||||
|
|
||||||
private IRepository<EmailOut> EmailOutRepository => _provider.GetRequiredService<IRepository<EmailOut>>();
|
|
||||||
|
|
||||||
private MailParams MailParams => _provider.GetRequiredService<IOptions<MailParams>>().Value;
|
|
||||||
|
|
||||||
private DispatcherParams DispatcherParams => _provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
|
||||||
|
|
||||||
private IConfigService ConfigService => _provider.GetRequiredService<IConfigService>();
|
|
||||||
|
|
||||||
private IMediator Mediator => _provider.GetRequiredService<IMediator>();
|
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,385 +40,10 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var now = DateTime.Now;
|
var envelope = request.UseSQLExecutor
|
||||||
var title = request.Title?.Trim() ?? string.Empty;
|
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel)
|
||||||
var message = request.Message?.Trim() ?? string.Empty;
|
: await Repository.CreateAsync(request, cancel);
|
||||||
|
|
||||||
Envelope envelope;
|
|
||||||
if (request.EnvelopeId is int envelopeId)
|
|
||||||
{
|
|
||||||
envelope = await Repository.Query
|
|
||||||
.Include(e => e.User)
|
|
||||||
.FirstOrDefaultAsync(e => e.Id == envelopeId, cancel)
|
|
||||||
?? throw new BadRequestException($"Envelope {envelopeId} not found.");
|
|
||||||
|
|
||||||
if (envelope.UserId != request.UserId)
|
|
||||||
throw new ForbiddenException();
|
|
||||||
|
|
||||||
await Repository.UpdateAsync(e =>
|
|
||||||
{
|
|
||||||
e.Title = title;
|
|
||||||
e.Message = message;
|
|
||||||
e.TfaEnabled = request.TFAEnabled;
|
|
||||||
e.Status = EnvelopeStatus.EnvelopeSaved;
|
|
||||||
e.ChangedWhen = now;
|
|
||||||
}, e => e.Id == envelopeId, cancel);
|
|
||||||
|
|
||||||
envelope.Title = title;
|
|
||||||
envelope.Message = message;
|
|
||||||
envelope.TfaEnabled = request.TFAEnabled;
|
|
||||||
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
|
||||||
envelope.ChangedWhen = now;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
envelope = request.UseSQLExecutor
|
|
||||||
? await Executor.CreateEnvelopeAsync(request.UserId, title, message, request.TFAEnabled, cancel)
|
|
||||||
: await Repository.CreateAsync(new Envelope
|
|
||||||
{
|
|
||||||
UserId = request.UserId,
|
|
||||||
Status = EnvelopeStatus.EnvelopeCreated,
|
|
||||||
Uuid = Guid.NewGuid().ToString(),
|
|
||||||
Title = title,
|
|
||||||
Message = message,
|
|
||||||
AddedWhen = now,
|
|
||||||
ChangedWhen = now,
|
|
||||||
Comment = string.Empty,
|
|
||||||
Language = "de-DE",
|
|
||||||
TfaEnabled = request.TFAEnabled,
|
|
||||||
UseAccessCode = false,
|
|
||||||
SendReminderEmails = false
|
|
||||||
}, cancel);
|
|
||||||
|
|
||||||
await Mediator.Send(new CreateHistoryCommand
|
|
||||||
{
|
|
||||||
EnvelopeId = envelope.Id,
|
|
||||||
UserReference = await ResolveSenderReferenceAsync(envelope, cancel),
|
|
||||||
Status = EnvelopeStatus.EnvelopeCreated
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
await UpsertDocumentAsync(envelope.Id, request.Document, now, cancel);
|
|
||||||
await UpsertReceiversAndFieldsAsync(envelope.Id, request.Receivers, now, cancel);
|
|
||||||
|
|
||||||
if (request.Send)
|
|
||||||
await SendEnvelopeAsync(envelope, cancel);
|
|
||||||
|
|
||||||
envelope = await Repository.Query
|
|
||||||
.Include(e => e.User)
|
|
||||||
.Include(e => e.EnvelopeReceivers)
|
|
||||||
.ThenInclude(er => er.Receiver)
|
|
||||||
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
|
||||||
|
|
||||||
return _mapper.Map<EnvelopeDto>(envelope);
|
return _mapper.Map<EnvelopeDto>(envelope);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
|
|
||||||
return envelope.User.Email;
|
|
||||||
|
|
||||||
var senderEmail = await Repository.Query
|
|
||||||
.Where(e => e.Id == envelope.Id)
|
|
||||||
.Select(e => e.User.Email)
|
|
||||||
.FirstOrDefaultAsync(cancel);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(senderEmail))
|
|
||||||
throw new BadRequestException("Envelope sender email is missing.");
|
|
||||||
|
|
||||||
return senderEmail;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpsertDocumentAsync(int envelopeId, CreateEnvelopeDocumentDto? document, DateTime now, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
if (document is null || string.IsNullOrWhiteSpace(document.DataAsBase64))
|
|
||||||
return;
|
|
||||||
|
|
||||||
byte[] bytes;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bytes = Convert.FromBase64String(document.DataAsBase64);
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Document payload is not valid base64.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingDocumentId = await DocumentRepository.Query
|
|
||||||
.Where(d => d.EnvelopeId == envelopeId)
|
|
||||||
.Select(d => (int?)d.Id)
|
|
||||||
.FirstOrDefaultAsync(cancel);
|
|
||||||
|
|
||||||
if (existingDocumentId is null)
|
|
||||||
{
|
|
||||||
await DocumentRepository.CreateAsync(new Document
|
|
||||||
{
|
|
||||||
EnvelopeId = envelopeId,
|
|
||||||
AddedWhen = now,
|
|
||||||
ByteData = bytes,
|
|
||||||
Filename = string.Empty,
|
|
||||||
Filepath = string.Empty,
|
|
||||||
FileNameOriginal = string.Empty
|
|
||||||
}, cancel);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await DocumentRepository.UpdateAsync(d =>
|
|
||||||
{
|
|
||||||
d.ByteData = bytes;
|
|
||||||
}, d => d.Id == existingDocumentId.Value, cancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpsertReceiversAndFieldsAsync(int envelopeId, IReadOnlyCollection<CreateEnvelopeReceiverDto> receivers, DateTime now, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var filteredReceivers = receivers
|
|
||||||
.Where(r => !string.IsNullOrWhiteSpace(r.EmailAddress))
|
|
||||||
.Select(r => r with
|
|
||||||
{
|
|
||||||
EmailAddress = r.EmailAddress.Trim().ToLowerInvariant(),
|
|
||||||
Name = r.Name?.Trim(),
|
|
||||||
PhoneNumber = string.IsNullOrWhiteSpace(r.PhoneNumber) ? null : r.PhoneNumber.Trim(),
|
|
||||||
Fields = r.Fields ?? new List<CreateEnvelopeFieldDto>()
|
|
||||||
})
|
|
||||||
.GroupBy(r => r.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(g => g.First())
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (filteredReceivers.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var document = await DocumentRepository.Query
|
|
||||||
.FirstOrDefaultAsync(d => d.EnvelopeId == envelopeId, cancel);
|
|
||||||
|
|
||||||
if (document is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var currentLinks = await EnvelopeReceiverRepository.Query
|
|
||||||
.Include(er => er.Receiver)
|
|
||||||
.Where(er => er.EnvelopeId == envelopeId)
|
|
||||||
.ToListAsync(cancel);
|
|
||||||
|
|
||||||
var linksByEmail = currentLinks
|
|
||||||
.Where(er => !string.IsNullOrWhiteSpace(er.Receiver?.EmailAddress))
|
|
||||||
.GroupBy(er => er.Receiver!.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.First(), StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var sequence = currentLinks.Count > 0 ? currentLinks.Max(er => er.Sequence) : 0;
|
|
||||||
|
|
||||||
foreach (var receiverDraft in filteredReceivers)
|
|
||||||
{
|
|
||||||
var linkResult = await GetOrCreateEnvelopeReceiverAsync(envelopeId, receiverDraft, linksByEmail, now, sequence, cancel);
|
|
||||||
var link = linkResult.Link;
|
|
||||||
sequence = linkResult.NextSequence;
|
|
||||||
|
|
||||||
await DocReceiverElementRepository.DeleteAsync(query => query
|
|
||||||
.Where(e => e.DocumentId == document.Id)
|
|
||||||
.Where(e => e.ReceiverId == link.ReceiverId), cancel);
|
|
||||||
|
|
||||||
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
|
|
||||||
{
|
|
||||||
await DocReceiverElementRepository.CreateAsync(new DocReceiverElement
|
|
||||||
{
|
|
||||||
DocumentId = document.Id,
|
|
||||||
ReceiverId = link.ReceiverId,
|
|
||||||
ElementType = 1,
|
|
||||||
X = field.X,
|
|
||||||
Y = field.Y,
|
|
||||||
Width = 1.77,
|
|
||||||
Height = 1.96,
|
|
||||||
Page = field.Page,
|
|
||||||
Required = true,
|
|
||||||
ReadOnly = false,
|
|
||||||
AnnotationIndex = 0,
|
|
||||||
AddedWhen = now,
|
|
||||||
FullName = link.Name,
|
|
||||||
Position = string.Empty,
|
|
||||||
Place = string.Empty,
|
|
||||||
Tooltip = string.Empty
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(EnvelopeReceiver Link, int NextSequence)> GetOrCreateEnvelopeReceiverAsync(
|
|
||||||
int envelopeId,
|
|
||||||
CreateEnvelopeReceiverDto receiverDraft,
|
|
||||||
IDictionary<string, EnvelopeReceiver> linksByEmail,
|
|
||||||
DateTime now,
|
|
||||||
int sequence,
|
|
||||||
CancellationToken cancel)
|
|
||||||
{
|
|
||||||
if (linksByEmail.TryGetValue(receiverDraft.EmailAddress, out var existingLink))
|
|
||||||
{
|
|
||||||
var nextName = string.IsNullOrWhiteSpace(receiverDraft.Name) ? existingLink.Name : receiverDraft.Name!;
|
|
||||||
var nextPhoneNumber = receiverDraft.PhoneNumber ?? existingLink.PhoneNumber;
|
|
||||||
|
|
||||||
await EnvelopeReceiverRepository.UpdateAsync(er =>
|
|
||||||
{
|
|
||||||
er.Name = nextName;
|
|
||||||
er.PhoneNumber = nextPhoneNumber;
|
|
||||||
er.ChangedWhen = now;
|
|
||||||
}, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel);
|
|
||||||
|
|
||||||
existingLink.Name = nextName;
|
|
||||||
existingLink.PhoneNumber = nextPhoneNumber;
|
|
||||||
return (existingLink, sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
var receiver = await ReceiverRepository.Query
|
|
||||||
.FirstOrDefaultAsync(r => r.EmailAddress != null && r.EmailAddress.ToLower() == receiverDraft.EmailAddress, cancel);
|
|
||||||
|
|
||||||
if (receiver is null)
|
|
||||||
{
|
|
||||||
receiver = await ReceiverRepository.CreateAsync(new Receiver
|
|
||||||
{
|
|
||||||
EmailAddress = receiverDraft.EmailAddress,
|
|
||||||
Signature = receiverDraft.EmailAddress.ToUpperInvariant().GetChecksum(),
|
|
||||||
AddedWhen = now,
|
|
||||||
TotpSecretkey = string.Empty,
|
|
||||||
TfaRegDeadline = null
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
sequence++;
|
|
||||||
var link = await EnvelopeReceiverRepository.CreateAsync(new EnvelopeReceiver
|
|
||||||
{
|
|
||||||
EnvelopeId = envelopeId,
|
|
||||||
ReceiverId = receiver.Id,
|
|
||||||
Sequence = sequence,
|
|
||||||
Name = receiverDraft.Name ?? receiverDraft.EmailAddress,
|
|
||||||
JobTitle = string.Empty,
|
|
||||||
CompanyName = string.Empty,
|
|
||||||
PrivateMessage = string.Empty,
|
|
||||||
AccessCode = string.Empty,
|
|
||||||
AddedWhen = now,
|
|
||||||
ChangedWhen = now,
|
|
||||||
PhoneNumber = receiverDraft.PhoneNumber ?? string.Empty
|
|
||||||
}, cancel);
|
|
||||||
|
|
||||||
link.Receiver = receiver;
|
|
||||||
linksByEmail[receiverDraft.EmailAddress] = link;
|
|
||||||
return (link, sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var hasDocument = await DocumentRepository.Query
|
|
||||||
.AnyAsync(d => d.EnvelopeId == envelope.Id && d.ByteData != null && d.ByteData.Length > 0, cancel);
|
|
||||||
|
|
||||||
if (!hasDocument)
|
|
||||||
throw new BadRequestException("Cannot send envelope without a document.");
|
|
||||||
|
|
||||||
var links = await EnvelopeReceiverRepository.Query
|
|
||||||
.Include(er => er.Receiver)
|
|
||||||
.Where(er => er.EnvelopeId == envelope.Id)
|
|
||||||
.ToListAsync(cancel);
|
|
||||||
|
|
||||||
if (links.Count == 0)
|
|
||||||
throw new BadRequestException("Cannot send envelope without receivers.");
|
|
||||||
|
|
||||||
var receiverIds = links.Select(l => l.ReceiverId).Distinct().ToList();
|
|
||||||
var documentIds = await DocumentRepository.Query
|
|
||||||
.Where(d => d.EnvelopeId == envelope.Id)
|
|
||||||
.Select(d => d.Id)
|
|
||||||
.ToListAsync(cancel);
|
|
||||||
|
|
||||||
var receiverFieldCounts = await DocReceiverElementRepository.Query
|
|
||||||
.Where(e => documentIds.Contains(e.DocumentId))
|
|
||||||
.Where(e => receiverIds.Contains(e.ReceiverId))
|
|
||||||
.GroupBy(e => e.ReceiverId)
|
|
||||||
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
|
||||||
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
|
||||||
|
|
||||||
var receiversWithoutFields = links
|
|
||||||
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
|
||||||
.Select(link => link.Name)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (receiversWithoutFields.Count > 0)
|
|
||||||
throw new BadRequestException("Cannot send envelope because some receivers have no signature fields.");
|
|
||||||
|
|
||||||
var changedWhen = DateTime.Now;
|
|
||||||
await Repository.UpdateAsync(e =>
|
|
||||||
{
|
|
||||||
e.Status = EnvelopeStatus.EnvelopeQueued;
|
|
||||||
e.ChangedWhen = changedWhen;
|
|
||||||
}, e => e.Id == envelope.Id, cancel);
|
|
||||||
|
|
||||||
var userReference = await ResolveSenderReferenceAsync(envelope, cancel);
|
|
||||||
|
|
||||||
await Mediator.Send(new CreateHistoryCommand
|
|
||||||
{
|
|
||||||
EnvelopeId = envelope.Id,
|
|
||||||
UserReference = userReference,
|
|
||||||
Status = EnvelopeStatus.EnvelopeQueued
|
|
||||||
}, cancel);
|
|
||||||
|
|
||||||
await QueueInvitationEmailsAsync(envelope, links, userReference, cancel);
|
|
||||||
|
|
||||||
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task QueueInvitationEmailsAsync(
|
|
||||||
Envelope envelope,
|
|
||||||
IReadOnlyCollection<EnvelopeReceiver> links,
|
|
||||||
string senderReference,
|
|
||||||
CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var template = await EmailTemplateRepository.Query
|
|
||||||
.SingleOrDefaultAsync(x => x.Name == EmailTemplateType.DocumentReceived.ToString(), cancel)
|
|
||||||
?? throw new InvalidOperationException($"Email template not found: {EmailTemplateType.DocumentReceived}");
|
|
||||||
|
|
||||||
var signatureHost = await ConfigService.ReadDefaultSignatureHost();
|
|
||||||
var senderName = senderReference;
|
|
||||||
|
|
||||||
foreach (var link in links)
|
|
||||||
{
|
|
||||||
var receiver = link.Receiver;
|
|
||||||
if (receiver is null || string.IsNullOrWhiteSpace(receiver.EmailAddress) || string.IsNullOrWhiteSpace(receiver.Signature))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var envelopeKey = (envelope.Uuid, receiver.Signature).ToEnvelopeKey();
|
|
||||||
var documentLink = $"{signatureHost.TrimEnd('/')}/envelope/login/{Uri.EscapeDataString(envelopeKey)}";
|
|
||||||
|
|
||||||
var placeholders = new Dictionary<string, string>(MailParams.Placeholders)
|
|
||||||
{
|
|
||||||
["[NAME_RECEIVER]"] = link.Name ?? string.Empty,
|
|
||||||
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
|
||||||
["[DOCUMENT_TITLE]"] = envelope.Title ?? string.Empty,
|
|
||||||
["[MESSAGE]"] = envelope.Message ?? string.Empty,
|
|
||||||
["[LINK_TO_DOCUMENT]"] = documentLink,
|
|
||||||
["[LINK_TO_DOCUMENT_TEXT]"] = documentLink[..Math.Min(40, documentLink.Length)] + ".."
|
|
||||||
};
|
|
||||||
|
|
||||||
var subject = ReplacePlaceHolders(template.Subject, placeholders);
|
|
||||||
var body = ReplacePlaceHolders(template.Body, placeholders);
|
|
||||||
|
|
||||||
var emailOut = new EmailOut
|
|
||||||
{
|
|
||||||
EmailAddress = receiver.EmailAddress,
|
|
||||||
EmailSubj = subject,
|
|
||||||
EmailBody = body,
|
|
||||||
AddedWhen = DateTime.Now,
|
|
||||||
AddedWho = DispatcherParams.AddedWho,
|
|
||||||
SendingProfile = DispatcherParams.SendingProfile,
|
|
||||||
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
|
||||||
EmailAttmt1 = DispatcherParams.EmailAttmt1,
|
|
||||||
WfId = (int)EnvelopeStatus.MessageInvitationSent,
|
|
||||||
ReferenceString = receiver.EmailAddress,
|
|
||||||
ReferenceId = link.ReceiverId
|
|
||||||
};
|
|
||||||
|
|
||||||
await EmailOutRepository.CreateAsync(emailOut, cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReplacePlaceHolders(string text, Dictionary<string, string> placeHolders)
|
|
||||||
{
|
|
||||||
foreach (var placeHolder in placeHolders)
|
|
||||||
text = text.Replace(placeHolder.Key, placeHolder.Value);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|
||||||
|
|
||||||
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
|
|
||||||
|
|
||||||
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public int EnvelopeId { get; init; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[MinLength(1)]
|
|
||||||
public string Reason { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
internal int UserId { get; private set; }
|
|
||||||
|
|
||||||
public DeleteEnvelopeCommand WithAuth(int userId)
|
|
||||||
{
|
|
||||||
UserId = userId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Application.Histories.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|
||||||
|
|
||||||
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
|
||||||
{
|
|
||||||
private readonly IRepository<Envelope> _envelopeRepository;
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
public DeleteEnvelopeCommandHandler(
|
|
||||||
IRepository<Envelope> envelopeRepository,
|
|
||||||
IMediator mediator)
|
|
||||||
{
|
|
||||||
_envelopeRepository = envelopeRepository;
|
|
||||||
_mediator = mediator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var reason = request.Reason?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(reason))
|
|
||||||
throw new BadRequestException("Please provide a reason.");
|
|
||||||
|
|
||||||
var envelope = await _envelopeRepository.Query
|
|
||||||
.Include(e => e.User)
|
|
||||||
.Include(e => e.EnvelopeReceivers)
|
|
||||||
.ThenInclude(er => er.Receiver)
|
|
||||||
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
|
||||||
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
|
||||||
|
|
||||||
if (envelope.UserId != request.UserId)
|
|
||||||
throw new ForbiddenException();
|
|
||||||
|
|
||||||
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
|
||||||
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
|
||||||
|
|
||||||
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
|
||||||
? EnvelopeStatus.EnvelopeWithdrawn
|
|
||||||
: EnvelopeStatus.EnvelopeDeleted;
|
|
||||||
|
|
||||||
var changedWhen = DateTime.Now;
|
|
||||||
envelope.Status = targetStatus;
|
|
||||||
envelope.Comment = reason;
|
|
||||||
envelope.ChangedWhen = changedWhen;
|
|
||||||
|
|
||||||
await _envelopeRepository.UpdateAsync(e =>
|
|
||||||
{
|
|
||||||
e.Status = targetStatus;
|
|
||||||
e.Comment = reason;
|
|
||||||
e.ChangedWhen = changedWhen;
|
|
||||||
}, e => e.Id == envelope.Id, cancel);
|
|
||||||
|
|
||||||
var userReference = envelope.User?.Email;
|
|
||||||
if (string.IsNullOrWhiteSpace(userReference))
|
|
||||||
throw new BadRequestException("Envelope sender email is missing.");
|
|
||||||
|
|
||||||
await _mediator.Send(new CreateHistoryCommand
|
|
||||||
{
|
|
||||||
EnvelopeId = envelope.Id,
|
|
||||||
UserReference = userReference,
|
|
||||||
Status = targetStatus,
|
|
||||||
Comment = reason
|
|
||||||
}, cancel);
|
|
||||||
|
|
||||||
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
|
||||||
{
|
|
||||||
if (envelopeReceiver.Receiver is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
await _mediator.Publish(new EnvelopeDeletedNotification
|
|
||||||
{
|
|
||||||
Envelope = envelope,
|
|
||||||
EnvelopeReceiver = envelopeReceiver,
|
|
||||||
Reason = reason,
|
|
||||||
SenderReference = userReference
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,16 +14,6 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public bool OnlyActive { get; init; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public bool OnlyCompleted { get; init; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage des Include des Umschlags
|
/// Abfrage des Include des Umschlags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -32,7 +22,7 @@ public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<Envelo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal int? UserId { get; init; }
|
public int? UserId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Setzt den Benutzerkontext für die Abfrage.
|
/// Setzt den Benutzerkontext für die Abfrage.
|
||||||
@@ -142,18 +132,10 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request is { OnlyActive: true })
|
|
||||||
query = query.Where(e => Status.Active.Contains(e.Status));
|
|
||||||
|
|
||||||
if (request is { OnlyCompleted: true })
|
|
||||||
query = query.Where(e => Status.Completed.Contains(e.Status));
|
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
.Include(e => e.Type)
|
.Include(e => e.Documents)
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
|
||||||
.Include(e => e.Histories)
|
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ public record CreateHistoryCommand : EnvelopeReceiverQueryBase, IRequest<History
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime AddedWhen { get; } = DateTime.Now;
|
public DateTime AddedWhen { get; } = DateTime.UtcNow;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user