Compare commits
1 Commits
refactor/w
...
bugfix/leg
| Author | SHA1 | Date | |
|---|---|---|---|
| b76b59f37b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -370,4 +370,3 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
/ignore/EnvelopeGenerator.Form
|
/ignore/EnvelopeGenerator.Form
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json
|
|
||||||
|
|||||||
241
AGENTS.md
241
AGENTS.md
@@ -4,130 +4,35 @@
|
|||||||
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
||||||
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
|
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation Catalog
|
|
||||||
|
|
||||||
This section explains every markdown file in the repo: what it contains, why it exists, and when you need it. Read this before deciding which file to open.
|
|
||||||
|
|
||||||
### Core Architecture Docs (Active — Always Relevant)
|
|
||||||
|
|
||||||
#### `AGENTS.md` ← you are here
|
|
||||||
**What:** Authoritative agent/copilot onboarding guide. Covers project structure, route table, render mode rules, coordinate system, API gaps, and common mistakes.
|
|
||||||
**Why it exists:** Single source of truth for agents so they don't re-derive architecture from code on every session.
|
|
||||||
**When you need it:** Always. Read first on every session.
|
|
||||||
**Maintenance:** Update whenever project structure, routes, render modes, or naming conventions change.
|
|
||||||
|
|
||||||
#### `COPILOT_CONTEXT.md`
|
|
||||||
**What:** Deep-dive architecture reference. Covers the full Blazor Auto hybrid setup, authentication flows (JWT + cookies, per-envelope receiver tokens), service registration patterns, coordinate system proof, and an explicit "prefer Server/Server.Client over old WebUI/ReceiverUI" rule.
|
|
||||||
**Why it exists:** Prevents agents from re-deriving or misinterpreting the hybrid SSR+WASM architecture. Also documents coordinate system evidence to prevent regressions (inches vs. points vs. pixels).
|
|
||||||
**When you need it:**
|
|
||||||
- Implementing any new page or service
|
|
||||||
- Debugging auth/cookie issues
|
|
||||||
- Any coordinate conversion question
|
|
||||||
- Unsure which project a new file should go in
|
|
||||||
**Maintenance:** Update when render mode assignments change, new auth schemes are added, or the coordinate system is touched.
|
|
||||||
|
|
||||||
#### `FORM_APPLICATION_CONTEXT.md`
|
|
||||||
**What:** Comprehensive reverse-engineering of the legacy VB.NET WinForms app (`EnvelopeGenerator.Form`). Documents every form (frmMain, frmEnvelopeEditor, frmFieldEditor, etc.), their UI layout, toolbar actions, data models, coordinate system, and a feature-by-feature migration mapping table to `EnvelopeGenerator.Server`.
|
|
||||||
**Why it exists:** The WinForms app is the functional specification for the sender-side web UI. It documents the full intended workflow that is being migrated to Blazor — especially the envelope creation, signature field placement, and dashboard features that are not yet implemented.
|
|
||||||
**When you need it:**
|
|
||||||
- Implementing any sender-side feature (`/sender`, `/sender/editor`, `/sender/envelope/{id}`)
|
|
||||||
- Understanding what a specific toolbar button or form should do
|
|
||||||
- Designing API endpoints for sender workflow
|
|
||||||
- Checking coordinate system behavior (INCHES proof is here)
|
|
||||||
**Maintenance:** Update the mapping table as features are implemented (change `❌ Not implemented` → `✅ Exists`). Do not rename `ReceiverUI` back — it was the old project name; current target is `EnvelopeGenerator.Server`.
|
|
||||||
|
|
||||||
#### `RECEIVER_PDF_VIEWER_CONTEXT.md`
|
|
||||||
**What:** Detailed technical reference for the receiver-side PDF viewer (`EnvelopeReceiverPage.razor`). Documents the PDF.js integration, signature button rendering, JS interop API (`window.pdfViewer`), signature capture flow, HiDPI/zoom/thumbnail config, and the full component lifecycle.
|
|
||||||
**Why it exists:** The receiver PDF viewer is the most complex component in the codebase. This doc prevents agents from misunderstanding the JS↔Blazor boundary, re-implementing already-solved problems (HiDPI, zoom, signature caching), or breaking the signing flow.
|
|
||||||
**When you need it:**
|
|
||||||
- Working on `EnvelopeReceiverPage.razor` or `pdf-viewer.js`
|
|
||||||
- Adding new signature field rendering logic
|
|
||||||
- Debugging signature rendering, zoom, or thumbnail issues
|
|
||||||
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
|
|
||||||
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
|
|
||||||
|
|
||||||
#### `DATABASE_STRUCTURE_CONTEXT.md`
|
|
||||||
**What:** Deep DB documentation for the envelope domain. Includes table/column snapshot, entity-to-table mapping, EF relationship/constraint map, stored procedure behavior + call sites, and trigger behavior + wiring path.
|
|
||||||
**Why it exists:** DB is centralized and can evolve independently; this file preserves migration-critical data semantics when historical DB context is hard to recover.
|
|
||||||
**When you need it:**
|
|
||||||
- Planning or reviewing DB/schema changes
|
|
||||||
- Debugging status transitions/history side effects
|
|
||||||
- Tracing where `PRSIG_*` procedures or DB triggers affect app behavior
|
|
||||||
- Reconciling Domain entities with physical SQL schema
|
|
||||||
**Maintenance:** Refresh on each DB migration, trigger/procedure change, or significant Domain/EGDbContext model change.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Deployment Docs
|
|
||||||
|
|
||||||
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
|
||||||
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
|
|
||||||
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
|
|
||||||
**When you need it:**
|
|
||||||
- Deploying to IIS for the first time
|
|
||||||
- Troubleshooting 500 errors on production after deploy
|
|
||||||
- Setting up a new deployment environment
|
|
||||||
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Markdown Audit Snapshot (2026-10-05)
|
|
||||||
|
|
||||||
The following table summarizes repository markdown files by practical status: completed, partially complete, or removable candidates.
|
|
||||||
|
|
||||||
| File | Type | Status | Missing / Gap | Removal Recommendation |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `AGENTS.md` | Agent onboarding | Partially complete | Route/migration statements can drift from real code over time. | Keep |
|
|
||||||
| `COPILOT_CONTEXT.md` | Architecture reference | Partially complete | Some route/render details may be out of sync with other docs; needs regular reconciliation. | Keep |
|
|
||||||
| `FORM_APPLICATION_CONTEXT.md` | Legacy-to-modern feature spec | Partially complete | Large sections still marked not implemented / needs implementation. | Keep |
|
|
||||||
| `RECEIVER_PDF_VIEWER_CONTEXT.md` | Receiver PDF viewer deep-dive | Partially complete | Long migration plan needs periodic sync with active implementation. | Keep |
|
|
||||||
| `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` | Sender workflow implementation plan | Partially complete | No explicit implementation progress markers per phase. | Keep (until migration closes) |
|
|
||||||
| `SENDER_PDF_TOOLBAR_PLAN_EN.md` | Sender PDF toolbar plan | Incomplete | Explicit pending item remains (`Toolbar visual polish`). | Keep (until done) |
|
|
||||||
| `SENDER_EDITOR_WASM_PDF_EDITOR_IMPLEMENTATION_PLAN_en.md` | Sender editor WASM migration plan | Incomplete | Plan exists but completion/progress tracking is minimal. | Keep (until done) |
|
|
||||||
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md` | Deployment guide | Complete (operational) | No critical gap identified. | Keep |
|
|
||||||
| `envelope-generator-ui/README.md` | Angular scaffold README | Complete but generic | Template text only; low project-specific value. | Removable or replace with project-specific summary |
|
|
||||||
| `EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Third-party vendor README | Complete (vendor) | Not project workflow documentation. | Keep with vendor assets |
|
|
||||||
| `EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Third-party license | Complete (legal) | No gap. | Keep |
|
|
||||||
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md` | Third-party vendor README | Complete (vendor) | Not project-specific guidance. | Keep with vendor assets |
|
|
||||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/README.md` | Legacy PSPDFKit note | Outdated | Mentions deprecated PSPDFKit-based flow; may mislead if reused. | Removable/archive |
|
|
||||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Duplicate vendor README | Redundant | Duplicate copy under `ignore/`. | Removable/archive |
|
|
||||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/bootstrap-icons/README.md` | Vendor README in archive area | Secondary | Archive-only utility, not active docs. | Removable/archive |
|
|
||||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Duplicate license in archive area | Redundant | Duplicate copy under `ignore/`. | Removable/archive (if legal policy allows) |
|
|
||||||
| `ignore/EnvelopeGenerator/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/README.md` | Duplicate Angular scaffold README | Redundant | Generic template + duplicate in archive path. | Removable/archive |
|
|
||||||
|
|
||||||
### Documentation Audit TODO
|
|
||||||
|
|
||||||
- [ ] Validate this markdown audit table against the current codebase and update statuses (`complete`, `partial`, `incomplete`, `removable`) when implementation changes land.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Active Architecture (Post-Migration)
|
## Active Architecture (Post-Migration)
|
||||||
|
|
||||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||||
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
- **WebUI** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||||
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
- **WebUI.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||||
|
|
||||||
**Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
|
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0)
|
||||||
|
|
||||||
**Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
|
**Proxy:** YARP in WebUI routes `/api/*` → `localhost:8088` (API)
|
||||||
|
|
||||||
### Deprecated Projects - DO NOT USE
|
### Deprecated Projects - DO NOT USE
|
||||||
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to WebUI)
|
||||||
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
|
- `EnvelopeGenerator.Web` - Razor Pages (replaced by WebUI)
|
||||||
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Run Active Host
|
### Run Both Projects (Required)
|
||||||
```powershell
|
```powershell
|
||||||
# Terminal - Blazor + Web API host
|
# Terminal 1 - API Backend
|
||||||
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
cd EnvelopeGenerator.API
|
||||||
|
dotnet run
|
||||||
|
|
||||||
|
# Terminal 2 - Blazor Frontend
|
||||||
|
cd EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI
|
||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
|
|
||||||
**Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
|
**Critical:** Both must run simultaneously. WebUI proxy forwards `/api/*` to API.
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
```powershell
|
```powershell
|
||||||
@@ -140,26 +45,24 @@ dotnet build EnvelopeGenerator.sln
|
|||||||
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
|
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
|
||||||
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
||||||
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
||||||
EnvelopeGenerator.Server/
|
EnvelopeGenerator.API/ # Controllers, endpoints
|
||||||
EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
|
EnvelopeGenerator.WebUI/ # Server-side Blazor components
|
||||||
├─ Components/Pages/ # @rendermode InteractiveServer
|
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||||
├─ Controllers/ # Active Web API endpoints
|
EnvelopeGenerator.WebUI.Client/ # Client-side WASM components
|
||||||
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||||
├─ Pages/ # @rendermode InteractiveWebAssembly
|
├─ Services/ # HTTP API clients
|
||||||
├─ Services/ # HTTP API clients
|
├─ Models/ # DTOs
|
||||||
├─ Models/ # DTOs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Route Structure (Critical)
|
## Route Structure (Critical)
|
||||||
|
|
||||||
| Route | File Location | Render Mode | Purpose |
|
| Route | File Location | Render Mode | Purpose |
|
||||||
|-------|--------------|-------------|---------|
|
|-------|--------------|-------------|---------|
|
||||||
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
|
| `/` | `WebUI.Client/Pages/Index.razor` | WASM | Landing page |
|
||||||
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
| `/sender/login` | `WebUI.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
||||||
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
|
| `/sender` | `WebUI.Client/Pages/EnvelopeSenderPage.razor` | WASM | Sender dashboard |
|
||||||
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
|
| `/envelope/login/{key}` | `WebUI.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
||||||
| `/envelope/login/{key}` | `Server.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
| `/envelope/{key}` | `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
|
||||||
| `/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.
|
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
|
||||||
|
|
||||||
@@ -192,20 +95,22 @@ float inches = (pixelX / canvasWidth) * pageWidthInches;
|
|||||||
- **Use case:** External API consumers
|
- **Use case:** External API consumers
|
||||||
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
|
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
|
||||||
|
|
||||||
### Sender Workflow Endpoint
|
### Missing Granular Endpoints (Need to Create)
|
||||||
```
|
```
|
||||||
POST /api/Envelope # Create/update/save/send envelope workflow payload
|
POST /api/Envelope/draft # Create draft envelope
|
||||||
|
PUT /api/Envelope/{id} # Update metadata
|
||||||
DELETE /api/Envelope/{id} # Delete with reason
|
DELETE /api/Envelope/{id} # Delete with reason
|
||||||
POST /api/Envelope/{id}/document # Upload PDF
|
POST /api/Envelope/{id}/document # Upload PDF
|
||||||
POST /api/Envelope/{id}/receivers # Add receiver
|
POST /api/Envelope/{id}/receivers # Add receiver
|
||||||
POST /api/Envelope/{id}/signature-fields # Place signature field
|
POST /api/Envelope/{id}/signature-fields # Place signature field
|
||||||
|
POST /api/Envelope/{id}/send # Send to receivers
|
||||||
```
|
```
|
||||||
|
|
||||||
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
||||||
|
|
||||||
## Status Color Coding
|
## Status Color Coding
|
||||||
|
|
||||||
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
|
Form app uses DevExpress `CustomDrawCell`. WebUI needs CSS:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
||||||
@@ -218,10 +123,10 @@ Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
|
### YARP Proxy (`WebUI/yarp.json`)
|
||||||
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
||||||
|
|
||||||
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
|
### PDF.js Settings (`WebUI/wwwroot/appsettings.json`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"PdfViewerOptions": {
|
"PdfViewerOptions": {
|
||||||
@@ -233,7 +138,7 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
|
### API Config (`API/appsettings.json`)
|
||||||
- `ConnectionStrings:Default` - SQL Server DB
|
- `ConnectionStrings:Default` - SQL Server DB
|
||||||
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
||||||
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
||||||
@@ -271,84 +176,27 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
|||||||
**No automated tests exist yet.**
|
**No automated tests exist yet.**
|
||||||
|
|
||||||
Manual testing workflow:
|
Manual testing workflow:
|
||||||
1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
1. Start API (`dotnet run` in `EnvelopeGenerator.API`)
|
||||||
2. Navigate to `https://localhost:5131` (or check console output for port)
|
2. Start WebUI (`dotnet run` in `EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI`)
|
||||||
3. Test sender login at `/sender/login`
|
3. Navigate to `https://localhost:5131` (or check console output for port)
|
||||||
4. Test receiver flow at `/envelope/login/{envelopeKey}`
|
4. Test sender login at `/sender/login`
|
||||||
|
5. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||||
## Technical Debt TODOs
|
|
||||||
|
|
||||||
- **CRITICAL (Licensing / pre-commercial release):** Remove all `SixLabors.ImageSharp` and `QRCoder-ImageSharp` package dependencies from production/runtime projects before commercial release. Replace remaining QR/image paths with permissive-license alternatives (no copyleft/share-source obligations), run repo-wide package audit (`*.csproj`), and add CI guardrails to block reintroduction.
|
|
||||||
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
|
|
||||||
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
|
|
||||||
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
|
|
||||||
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
|
|
||||||
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
|
|
||||||
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
|
|
||||||
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
|
|
||||||
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
|
|
||||||
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
|
|
||||||
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
|
||||||
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
|
||||||
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
|
||||||
- Implement SPA-style language switching so the UI culture updates immediately without a full page reload.
|
|
||||||
- Implement sender-scoped caching for "recent receiver suggestions" used during envelope creation (wizard/editor flows): cache key should include sender identity, support short TTL + invalidation hooks after successful envelope receiver writes, and prevent cross-sender suggestion leakage.
|
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
**SQL Server** (DD_ECM)
|
**SQL Server** (DD_ECM)
|
||||||
- Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
|
- Connection string in `API/appsettings.json`
|
||||||
- EF Core migrations NOT used (manual SQL scripts)
|
- EF Core migrations NOT used (manual SQL scripts)
|
||||||
- Stored procedures: `PRSIG_*` prefix
|
- Stored procedures: `PRSIG_*` prefix
|
||||||
|
|
||||||
**Key Tables:**
|
**Key Tables:**
|
||||||
- `TBSIG_ENVELOPE` - Envelope metadata
|
- `TBSIG_ENVELOPE` - Envelope metadata
|
||||||
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
|
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
|
||||||
- `TBSIG_DOCUMENT_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
|
- `TBSIG_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
|
||||||
- `TBSIG_RECEIVER` - Receiver registry
|
- `TBSIG_RECEIVER` - Receiver registry
|
||||||
- `TBSIG_ENVELOPE_DOCUMENT` - PDF binary data
|
- `TBSIG_DOCUMENT` - PDF binary data
|
||||||
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
||||||
|
|
||||||
### Data Structure (Domain + EGDbContext)
|
|
||||||
|
|
||||||
Use this as the canonical quick map before touching sender/receiver workflow code.
|
|
||||||
|
|
||||||
#### Core Aggregate Shape
|
|
||||||
- `Envelope` (`TBSIG_ENVELOPE`) is the root aggregate for sending workflow.
|
|
||||||
- `Envelope -> Documents` is `1:N` via `Document.EnvelopeId` (`TBSIG_ENVELOPE_DOCUMENT`).
|
|
||||||
- `Envelope -> Histories` is `1:N` via `History.EnvelopeId` (`TBSIG_ENVELOPE_HISTORY`).
|
|
||||||
- `Envelope <-> Receiver` is `N:N` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{EnvelopeId, ReceiverId}`).
|
|
||||||
- `Document -> DocReceiverElement` is `1:N` via `DocReceiverElement.DocumentId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT`).
|
|
||||||
- `DocReceiverElement -> ElementAnnotation` is `1:N` via `ElementAnnotation.ElementId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`).
|
|
||||||
|
|
||||||
#### Status and Tracking Entities
|
|
||||||
- `DocumentStatus` (`TBSIG_DOCUMENT_STATUS`) links `EnvelopeId + ReceiverId` and stores receiver-level signing state.
|
|
||||||
- `History` stores timeline events (`EnvelopeStatus`) with polymorphic `UserReference` lookup semantics.
|
|
||||||
- `EnvelopeReceiverReadOnly` (`TBSIG_ENVELOPE_RECEIVER_READ_ONLY`) stores temporary read-only delegation with validity window (`DateValid`).
|
|
||||||
|
|
||||||
#### Important Constraints From `EGDbContext`
|
|
||||||
- `Envelope.Uuid` has a unique index.
|
|
||||||
- `Receiver.Signature` has a unique index.
|
|
||||||
- `Receiver.EmailAddress` has a unique index.
|
|
||||||
- `EnvelopeReceiver` uses composite primary key: `{ EnvelopeId, ReceiverId }`.
|
|
||||||
- `History.UserReference` is mapped conditionally (`IsMigration == false`) to:
|
|
||||||
- `Receiver.EmailAddress` (`History.Receiver`)
|
|
||||||
- `User.Email` from UserManager (`History.Sender`)
|
|
||||||
- `EnvelopeReceiverReadOnly.AddedWho` maps to `Receiver.EmailAddress` (principal key mapping, not receiver GUID).
|
|
||||||
|
|
||||||
#### Coordinate-Critical Fields
|
|
||||||
- Signature placement is persisted in `DocReceiverElement` as `X`, `Y`, `Width`, `Height`, `Page`.
|
|
||||||
- These coordinates are in INCHES (not PDF points, not pixels).
|
|
||||||
|
|
||||||
#### Cross-Module Tables in Same Context
|
|
||||||
- `EGDbContext` also includes UserManager entities: `User`, `Group`, `Module`, `GroupOfUser`, `ModuleOfUser`, `UserRep`, and `ClientUser`.
|
|
||||||
- Mail dispatcher entity `EmailOut` is also registered under `#if NET` and trigger-configurable.
|
|
||||||
|
|
||||||
#### Trigger-Aware Model Notes
|
|
||||||
- Trigger names are injected from `DbTriggerParams` in appsettings and applied via `ToTable(tb => tb.HasTrigger(...))`.
|
|
||||||
- Current trigger registration is explicit (`AddTrigger<T>()` calls), not attribute/reflection driven.
|
|
||||||
- Do not remove trigger mappings casually; this project depends on DB-side automation/audit behavior.
|
|
||||||
|
|
||||||
## DevExpress
|
## DevExpress
|
||||||
|
|
||||||
**License:** Commercial (v25.2.3)
|
**License:** Commercial (v25.2.3)
|
||||||
@@ -397,7 +245,7 @@ Each envelope maintains independent authentication state.
|
|||||||
**CDN:**
|
**CDN:**
|
||||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
|
|
||||||
**NuGet (Server.Client):**
|
**NuGet (WebUI.Client):**
|
||||||
- `DevExpress.Blazor.*` 25.2.3
|
- `DevExpress.Blazor.*` 25.2.3
|
||||||
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
||||||
|
|
||||||
@@ -411,4 +259,5 @@ Each envelope maintains independent authentication state.
|
|||||||
None required. All config in `appsettings.json`.
|
None required. All config in `appsettings.json`.
|
||||||
|
|
||||||
**Local dev ports:**
|
**Local dev ports:**
|
||||||
- App host (UI + API): `https://localhost:5131` or `http://localhost:7192`
|
- API: `https://localhost:8088`
|
||||||
|
- WebUI: `https://localhost:5131` or `http://localhost:7192`
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ This project contains:
|
|||||||
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||||
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||||
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||||
- `EnvelopeGenerator.API` — deprecated project (no longer part of the active solution/runtime path)
|
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
### Legacy / Do Not Touch
|
### Legacy / Do Not Touch
|
||||||
- `EnvelopeGenerator.Service`
|
- `EnvelopeGenerator.Service`
|
||||||
@@ -121,7 +121,7 @@ Current controller set includes:
|
|||||||
- `SignatureController`
|
- `SignatureController`
|
||||||
- `TfaRegistrationController`
|
- `TfaRegistrationController`
|
||||||
|
|
||||||
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,384 +0,0 @@
|
|||||||
# Database Structure Context
|
|
||||||
|
|
||||||
## Why this file exists
|
|
||||||
|
|
||||||
This is the deep-dive DB reference for migration work when schema can change and old DB knowledge may be lost.
|
|
||||||
|
|
||||||
- Scope: `EnvelopeGenerator.Domain` entities + `EnvelopeGenerator.Infrastructure/EGDbContext.cs` + live DB metadata snapshot from `DD_ECM`.
|
|
||||||
- Goal: make schema transitions predictable (what exists, how it is linked, where logic executes).
|
|
||||||
- Safety: all DB checks used read-only `SELECT` metadata queries; no data mutation.
|
|
||||||
|
|
||||||
## Snapshot metadata
|
|
||||||
|
|
||||||
- Source DB: `DD_ECM` on `SDD-VHP04-SQL19\DD_TESTING01`
|
|
||||||
- Snapshot date: `2026-10-20`
|
|
||||||
- App context: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
|
|
||||||
|
|
||||||
## Fast navigation
|
|
||||||
|
|
||||||
- High-level aggregate map: `AGENTS.md` -> `Database` section
|
|
||||||
- EF model config source: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
|
|
||||||
- Stored procedure wrappers:
|
|
||||||
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
|
|
||||||
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
|
|
||||||
- `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
|
|
||||||
- Direct SQL execution in controller:
|
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
|
|
||||||
|
|
||||||
## Core data model (ER-level)
|
|
||||||
|
|
||||||
- `Envelope` (`TBSIG_ENVELOPE`) is root aggregate.
|
|
||||||
- `Envelope (1) -> (N) Document` via `TBSIG_ENVELOPE_DOCUMENT.ENVELOPE_ID`.
|
|
||||||
- `Envelope (1) -> (N) History` via `TBSIG_ENVELOPE_HISTORY.ENVELOPE_ID`.
|
|
||||||
- `Envelope (N) <-> (N) Receiver` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{ENVELOPE_ID, RECEIVER_ID}`).
|
|
||||||
- `Document (1) -> (N) DocReceiverElement` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT.DOCUMENT_ID`.
|
|
||||||
- `DocReceiverElement (1) -> (N) ElementAnnotation` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION.ELEMENT_ID`.
|
|
||||||
- `DocumentStatus` links `Envelope + Receiver` state in `TBSIG_DOCUMENT_STATUS`.
|
|
||||||
|
|
||||||
## Entity <-> table mapping (code-level)
|
|
||||||
|
|
||||||
| Entity | Table | PK in code | PK in DB | Important relation notes |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `Envelope` | `TBSIG_ENVELOPE` | `Id` | `GUID` | Has `Documents`, `Histories`, `EnvelopeReceivers`; unique index on `Uuid` configured in EF |
|
|
||||||
| `EnvelopeReceiver` | `TBSIG_ENVELOPE_RECEIVER` | `{EnvelopeId, ReceiverId}` | `{ENVELOPE_ID, RECEIVER_ID}` | DB also has `GUID` identity (not modeled as entity PK) |
|
|
||||||
| `Document` | `TBSIG_ENVELOPE_DOCUMENT` | `Id` | `GUID` | FK `ENVELOPE_ID` |
|
|
||||||
| `DocReceiverElement` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT` | `Id` | `GUID` | Coordinates persisted here (`POSITION_X/Y`, `WIDTH`, `HEIGHT`, `PAGE`) |
|
|
||||||
| `ElementAnnotation` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION` | `Id` | `GUID` | FK `ELEMENT_ID` |
|
|
||||||
| `DocumentStatus` | `TBSIG_DOCUMENT_STATUS` | `Id` | `GUID` | FK `ENVELOPE_ID`, `RECEIVER_ID` |
|
|
||||||
| `History` | `TBSIG_ENVELOPE_HISTORY` | `Id` | `GUID` | `USER_REFERENCE` maps to `Receiver.EmailAddress` and `User.Email` when `IsMigration == false` |
|
|
||||||
| `Receiver` | `TBSIG_RECEIVER` | `Id` | `GUID` | Unique indexes in code: `Signature`, `EmailAddress` |
|
|
||||||
| `EnvelopeReceiverReadOnly` | `TBSIG_ENVELOPE_RECEIVER_READ_ONLY` | `Id` | `GUID` | `AddedWho` is FK-like mapping to `Receiver.EmailAddress` |
|
|
||||||
| `EnvelopeType` | `TBSIG_ENVELOPE_TYPE` | `Id` | `GUID` | Used as optional envelope defaults/template |
|
|
||||||
| `EmailTemplate` | `TBSIG_EMAIL_TEMPLATE` | `Id` | `GUID` | Trigger-managed `CHANGED_WHEN` |
|
|
||||||
| `EmailOut` (legacy/bridge) | `TBEMLP_EMAIL_OUT` | `Id` | `GUID` | Internal/obsolete domain class, still registered for DB access |
|
|
||||||
|
|
||||||
## Physical schema snapshot (table/column level)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Format per column: `COLUMN_NAME TYPE NULLABLE`.
|
|
||||||
- This section reflects live DB metadata at snapshot time.
|
|
||||||
|
|
||||||
### `TBSIG_ENVELOPE`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `USER_ID int NOT NULL`
|
|
||||||
- `STATUS int NOT NULL`
|
|
||||||
- `ENVELOPE_UUID nvarchar(36) NOT NULL`
|
|
||||||
- `MESSAGE nvarchar(max) NOT NULL`
|
|
||||||
- `EXPIRES_WHEN datetime NULL`
|
|
||||||
- `EXPIRES_WARNING_WHEN datetime NULL`
|
|
||||||
- `VERIFICATION_TYPE tinyint NOT NULL`
|
|
||||||
- `TITLE nvarchar(128) NULL`
|
|
||||||
- `CONTRACT_TYPE int NULL`
|
|
||||||
- `LANGUAGE nvarchar(5) NULL`
|
|
||||||
- `SEND_REMINDER_EMAILS bit NULL`
|
|
||||||
- `FIRST_REMINDER_DAYS int NULL`
|
|
||||||
- `REMINDER_INTERVAL_DAYS int NULL`
|
|
||||||
- `ENVELOPE_TYPE int NULL`
|
|
||||||
- `CERTIFICATION_TYPE int NULL`
|
|
||||||
- `USE_ACCESS_CODE bit NULL`
|
|
||||||
- `FINAL_EMAIL_TO_CREATOR int NULL`
|
|
||||||
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
|
|
||||||
- `EXPIRES_WHEN_DAYS int NULL`
|
|
||||||
- `EXPIRES_WARNING_WHEN_DAYS int NULL`
|
|
||||||
- `TFA_ENABLED bit NOT NULL`
|
|
||||||
- `DOC_RESULT varbinary(max) NULL`
|
|
||||||
- `LANG_CODE varchar(5) NOT NULL`
|
|
||||||
- `SOURCE varchar(10) NOT NULL`
|
|
||||||
- `COMMENT varchar(128) NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
|
|
||||||
### `TBSIG_ENVELOPE_RECEIVER`
|
|
||||||
|
|
||||||
- `ENVELOPE_ID int NOT NULL`
|
|
||||||
- `RECEIVER_ID int NOT NULL`
|
|
||||||
- `SEQUENCE int NOT NULL`
|
|
||||||
- `NAME nvarchar(128) NULL`
|
|
||||||
- `JOB_TITLE nvarchar(128) NULL`
|
|
||||||
- `COMPANY_NAME nvarchar(128) NULL`
|
|
||||||
- `PRIVATE_MESSAGE nvarchar(max) NULL`
|
|
||||||
- `ACCESS_CODE nvarchar(64) NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
- `GUID bigint NOT NULL`
|
|
||||||
- `PHONE_NUMBER varchar(20) NULL`
|
|
||||||
- `PARENT_ENVELOPE_RECEIVER_ID int NOT NULL`
|
|
||||||
|
|
||||||
### `TBSIG_ENVELOPE_DOCUMENT`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `ENVELOPE_ID int NOT NULL`
|
|
||||||
- `FILENAME nvarchar(256) NOT NULL`
|
|
||||||
- `FILEPATH nvarchar(256) NOT NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `FILENAME_ORIGINAL nvarchar(256) NULL`
|
|
||||||
- `BYTE_DATA varbinary(max) NULL`
|
|
||||||
|
|
||||||
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `DOCUMENT_ID int NOT NULL`
|
|
||||||
- `RECEIVER_ID int NOT NULL`
|
|
||||||
- `ELEMENT_TYPE int NOT NULL`
|
|
||||||
- `POSITION_X float NOT NULL`
|
|
||||||
- `POSITION_Y float NOT NULL`
|
|
||||||
- `WIDTH float NOT NULL`
|
|
||||||
- `HEIGHT float NOT NULL`
|
|
||||||
- `PAGE int NOT NULL`
|
|
||||||
- `REQUIRED bit NOT NULL`
|
|
||||||
- `TOOLTIP nvarchar(512) NULL`
|
|
||||||
- `READ_ONLY bit NOT NULL`
|
|
||||||
- `ANNOTATION_INDEX int NOT NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
- `CHANGED_WHO nvarchar(100) NULL`
|
|
||||||
- `FULL_NAME nvarchar(100) NULL`
|
|
||||||
- `POSITION nvarchar(100) NULL`
|
|
||||||
- `PLACE nvarchar(100) NULL`
|
|
||||||
- `INK varbinary(max) NULL`
|
|
||||||
|
|
||||||
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`
|
|
||||||
|
|
||||||
- `GUID bigint NOT NULL`
|
|
||||||
- `ELEMENT_ID int NOT NULL`
|
|
||||||
- `NAME nvarchar(100) NOT NULL`
|
|
||||||
- `VALUE nvarchar(max) NOT NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
- `CHANGED_WHO nvarchar(100) NULL`
|
|
||||||
- `TYPE nvarchar(50) NOT NULL`
|
|
||||||
- `POSITION_X float NULL`
|
|
||||||
- `POSITION_Y float NULL`
|
|
||||||
- `WIDTH float NULL`
|
|
||||||
- `HEIGHT float NULL`
|
|
||||||
|
|
||||||
### `TBSIG_DOCUMENT_STATUS`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `ENVELOPE_ID int NOT NULL`
|
|
||||||
- `RECEIVER_ID int NOT NULL`
|
|
||||||
- `STATUS int NOT NULL`
|
|
||||||
- `STATUS_CHANGED_WHEN datetime NULL`
|
|
||||||
- `VALUE nvarchar(max) NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
|
|
||||||
### `TBSIG_ENVELOPE_HISTORY`
|
|
||||||
|
|
||||||
- `GUID bigint NOT NULL`
|
|
||||||
- `ENVELOPE_ID int NOT NULL`
|
|
||||||
- `USER_REFERENCE nvarchar(128) NOT NULL`
|
|
||||||
- `STATUS int NOT NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `ACTION_DATE datetime NULL`
|
|
||||||
- `COMMENT nvarchar(max) NULL`
|
|
||||||
|
|
||||||
### `TBSIG_RECEIVER`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `EMAIL_ADDRESS nvarchar(128) NOT NULL`
|
|
||||||
- `SIGNATURE nvarchar(64) NOT NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `TOTP_SECRET_KEY nvarchar(max) NULL`
|
|
||||||
- `TFA_REG_DEADLINE datetime NULL`
|
|
||||||
- `CULTURE nvarchar(10) NULL`
|
|
||||||
|
|
||||||
### `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`
|
|
||||||
|
|
||||||
- `GUID bigint NOT NULL`
|
|
||||||
- `ENVELOPE_ID bigint NOT NULL`
|
|
||||||
- `RECEIVER_MAIL nvarchar(250) NOT NULL`
|
|
||||||
- `DATE_VALID date NOT NULL`
|
|
||||||
- `ADDED_WHO varchar(100) NOT NULL`
|
|
||||||
- `ADDED_WHEN datetime NULL`
|
|
||||||
- `CHANGED_WHO varchar(100) NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
|
|
||||||
### `TBSIG_ENVELOPE_TYPE`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `TITLE nvarchar(128) NOT NULL`
|
|
||||||
- `LANGUAGE nvarchar(5) NOT NULL`
|
|
||||||
- `EXPIRES_DAYS int NULL`
|
|
||||||
- `CERTIFICATION_TYPE int NULL`
|
|
||||||
- `USE_ACCESS_CODE bit NULL`
|
|
||||||
- `FINAL_EMAIL_TO_CREATOR int NULL`
|
|
||||||
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
- `EXPIRES_WARNING_DAYS int NULL`
|
|
||||||
- `SEND_REMINDER_EMAILS bit NULL`
|
|
||||||
- `FIRST_REMINDER_DAYS int NULL`
|
|
||||||
- `REMINDER_INTERVAL_DAYS int NULL`
|
|
||||||
- `CONTRACT_TYPE int NULL`
|
|
||||||
|
|
||||||
### `TBSIG_EMAIL_TEMPLATE`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `NAME nvarchar(64) NULL`
|
|
||||||
- `BODY nvarchar(max) NULL`
|
|
||||||
- `SUBJECT nvarchar(512) NULL`
|
|
||||||
- `ADDED_WHEN datetime NOT NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
- `LANG_CODE varchar(5) NOT NULL`
|
|
||||||
|
|
||||||
### `TBEMLP_EMAIL_OUT`
|
|
||||||
|
|
||||||
- `GUID int NOT NULL`
|
|
||||||
- `REMINDER_TYPE_ID int NOT NULL`
|
|
||||||
- `SENDING_PROFILE int NOT NULL`
|
|
||||||
- `REFERENCE_ID int NOT NULL`
|
|
||||||
- `REFERENCE_STRING varchar(200) NULL`
|
|
||||||
- `ENTITY_ID int NULL`
|
|
||||||
- `WF_ID int NOT NULL`
|
|
||||||
- `WF_REFERENCE varchar(200) NULL`
|
|
||||||
- `EMAIL_ADRESS varchar(1000) NOT NULL`
|
|
||||||
- `EMAIL_SUBJ varchar(500) NOT NULL`
|
|
||||||
- `EMAIL_BODY varchar(max) NOT NULL`
|
|
||||||
- `EMAIL_ATTMT1 varchar(512) NULL`
|
|
||||||
- `EMAIL_SENT datetime NULL`
|
|
||||||
- `COMMENT varchar(500) NULL`
|
|
||||||
- `ADDED_WHO varchar(50) NOT NULL`
|
|
||||||
- `ADDED_WHEN datetime NULL`
|
|
||||||
- `CHANGED_WHO varchar(50) NULL`
|
|
||||||
- `CHANGED_WHEN datetime NULL`
|
|
||||||
- `ERROR_TIMESTAMP datetime NULL`
|
|
||||||
- `ERROR_MSG varchar(900) NULL`
|
|
||||||
- `ATTACHMENT1_BYTES varbinary(max) NULL`
|
|
||||||
- `ATT1_RELATED_ID bigint NULL`
|
|
||||||
- `ATT1_REL_TYPE varchar(20) NULL`
|
|
||||||
|
|
||||||
## Constraints and indexes (relevant)
|
|
||||||
|
|
||||||
From EF + DB metadata snapshot:
|
|
||||||
|
|
||||||
- `TBSIG_ENVELOPE`: PK `PK_TBSIG_ENVELOPE (GUID)`.
|
|
||||||
- `TBSIG_ENVELOPE_RECEIVER`: composite PK `PK_TBSIG_ENVELOPE_RECEIVER (ENVELOPE_ID, RECEIVER_ID)`.
|
|
||||||
- `TBSIG_DOCUMENT_RECEIVER_ELEMENT`: PK `GUID`, plus unique key `UQTBSIG_DOCUMENT_RECEIVER_ELEMENT_DocID_PosX_PosY (DOCUMENT_ID, POSITION_X, POSITION_Y)`.
|
|
||||||
- `TBSIG_RECEIVER`: PK `GUID`, unique key `UQTBSIG_RECEIVER (EMAIL_ADDRESS)`.
|
|
||||||
- `TBEMLP_EMAIL_OUT`: PK `GUID`, FK `FK_TBEMLP_EMAIL_OUT_SENDING_PROFILE`.
|
|
||||||
|
|
||||||
## Stored procedures: behavior and code call sites
|
|
||||||
|
|
||||||
### Active in .NET code path
|
|
||||||
|
|
||||||
1) `PRSIG_API_CREATE_ENVELOPE`
|
|
||||||
- Purpose: creates envelope row (`STATUS=1002`, `SOURCE='API'`, defaults) and returns `ENVELOPE_UUID` via output.
|
|
||||||
- DB writes: `TBSIG_ENVELOPE`.
|
|
||||||
- Called by:
|
|
||||||
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
|
|
||||||
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeExecutor.cs`
|
|
||||||
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
|
|
||||||
|
|
||||||
2) `PRSIG_API_CREATE_RECEIVER`
|
|
||||||
- Purpose: upserts `TBSIG_RECEIVER` by email, adds envelope assignment row with generated access code.
|
|
||||||
- DB writes: `TBSIG_RECEIVER`, `TBSIG_ENVELOPE_RECEIVER`.
|
|
||||||
- Called by:
|
|
||||||
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
|
|
||||||
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeReceiverExecutor.cs`
|
|
||||||
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
|
|
||||||
|
|
||||||
3) `PRSIG_API_ADD_DOC`
|
|
||||||
- Purpose: stores one document for envelope if absent; returns doc id.
|
|
||||||
- DB writes: `TBSIG_ENVELOPE_DOCUMENT`.
|
|
||||||
- Called by:
|
|
||||||
- SQL template `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
|
|
||||||
- executor `EnvelopeGenerator.Infrastructure/Executor/DocumentExecutor.cs`
|
|
||||||
- used in `EnvelopeReceiverController.CreateAsync`.
|
|
||||||
|
|
||||||
4) `PRSIG_API_ADD_DOC_RECEIVER_ELEM`
|
|
||||||
- Purpose: inserts signature element record if `(DOC_ID, RECEIVER_ID)` pair does not exist.
|
|
||||||
- DB writes: `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
|
|
||||||
- Called directly by:
|
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
|
|
||||||
|
|
||||||
5) `PRSIG_API_ADD_HISTORY_STATE`
|
|
||||||
- Purpose: appends history row with status and user reference.
|
|
||||||
- DB writes: `TBSIG_ENVELOPE_HISTORY`.
|
|
||||||
- Called directly by:
|
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
|
|
||||||
|
|
||||||
### Legacy/other path references in repo
|
|
||||||
|
|
||||||
- `PRSIG_GET_ENVELOPES_FOR_USER` and `PRSIG_GET_CLOSED_ENVELOPES_FOR_USER` are called in legacy VB path:
|
|
||||||
- `EnvelopeGenerator.CommonServices/Models/EnvelopeModel.vb`
|
|
||||||
|
|
||||||
- `PRSIG_API_WITHDRAW_ENVELOPE` exists in DB snapshot but no active `.cs` call site found in current server flow.
|
|
||||||
|
|
||||||
## Trigger behavior and where they are wired
|
|
||||||
|
|
||||||
## How trigger names are wired in app
|
|
||||||
|
|
||||||
- Config source: `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json` -> `DbTriggerParams`.
|
|
||||||
- DI binding: `EnvelopeGenerator.Infrastructure/DependencyInjection.cs` (`AddDbTriggerParams`).
|
|
||||||
- Model binding: `EnvelopeGenerator.Infrastructure/EGDbContext.cs` -> `AddTrigger<T>()` -> `ToTable(tb => tb.HasTrigger(triggerName))`.
|
|
||||||
|
|
||||||
### Trigger definitions observed in DB snapshot
|
|
||||||
|
|
||||||
1) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS`
|
|
||||||
- Fires on insert of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
|
|
||||||
- Normalizes/rewrites `POSITION_X`, `POSITION_Y` and sets `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_INS'`.
|
|
||||||
|
|
||||||
2) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD`
|
|
||||||
- Fires on update of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
|
|
||||||
- Rewrites coordinates and `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_UPD'` except when `CHANGED_WHO='OFFSET_Y_POS'`.
|
|
||||||
|
|
||||||
3) `TBSIG_ENVELOPE_HISTORY_AFT_INS`
|
|
||||||
- Fires on history insert.
|
|
||||||
- Central state machine side-effects:
|
|
||||||
- syncs `TBSIG_ENVELOPE.STATUS` for selected `100x` states,
|
|
||||||
- on `1003` applies Y-offset update to receiver elements,
|
|
||||||
- on `2005` calculates partial/fully signed and may insert follow-up `1006` history,
|
|
||||||
- on `2007` (type-dependent reject path) writes rejection emails into `TBEMLP_EMAIL_OUT` and updates envelope rejected state,
|
|
||||||
- on `3001` invitation-message counting can move envelope to `1004`.
|
|
||||||
|
|
||||||
4) `TBSIG_EMAIL_TEMPLATE_AFT_UPD`
|
|
||||||
- Fires on update of `TBSIG_EMAIL_TEMPLATE`.
|
|
||||||
- Sets `CHANGED_WHEN = GETDATE()`.
|
|
||||||
|
|
||||||
5) `TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD`
|
|
||||||
- Fires on update of `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`.
|
|
||||||
- Updates config timestamp row (`TBSIG_CONFIG.GUID = 1`).
|
|
||||||
|
|
||||||
6) `TBEMLP_EMAIL_OUT_AFT_UPD`
|
|
||||||
- Fires on update of `TBEMLP_EMAIL_OUT`.
|
|
||||||
- When `EMAIL_SENT` changes and `WF_ID in (3001..3005)`, writes corresponding history entries to `TBSIG_ENVELOPE_HISTORY`.
|
|
||||||
|
|
||||||
### Trigger mismatch alert (important)
|
|
||||||
|
|
||||||
- `DbTriggerParams` contains `TBEMLP_EMAIL_OUT_AFT_INS`, but this trigger was not found in the snapshot DB (`ExistsInDb = 0`).
|
|
||||||
- `TBEMLP_EMAIL_OUT_AFT_UPD` exists and is active.
|
|
||||||
|
|
||||||
This should be treated as environment drift and validated before migrations.
|
|
||||||
|
|
||||||
## Known model-vs-DB drift risks
|
|
||||||
|
|
||||||
These are not necessarily bugs, but they are migration risk hotspots:
|
|
||||||
|
|
||||||
- `Receiver.EmailAddress` is `[StringLength(250)]` in entity, DB column is `nvarchar(128)`.
|
|
||||||
- `EnvelopeReceiverReadOnly.AddedWho` entity expects `nvarchar(250)`, DB column is `varchar(100)`.
|
|
||||||
- `EnvelopeReceiver` entity does not model DB columns `GUID` and `PARENT_ENVELOPE_RECEIVER_ID`.
|
|
||||||
- `Envelope` entity does not explicitly model some DB columns (`VERIFICATION_TYPE`, `LANG_CODE`, `SOURCE`).
|
|
||||||
- `EmailOut` entity has `[NotMapped] AddedWhen`, while DB has `ADDED_WHEN` column.
|
|
||||||
|
|
||||||
Before schema changes, reconcile these deltas intentionally (either mapping update or documented accepted divergence).
|
|
||||||
|
|
||||||
## Migration-safe operating rules
|
|
||||||
|
|
||||||
- Do not rely on implicit trigger behavior; document expected side effects in feature code touching history/status flows.
|
|
||||||
- Any change to `TBSIG_ENVELOPE_HISTORY` semantics requires trigger impact review first.
|
|
||||||
- Keep `DbTriggerParams` synchronized with actual DB trigger inventory per environment.
|
|
||||||
- For stored procedure changes, update both:
|
|
||||||
- SQL wrapper templates (`Application/Common/SQL/*`)
|
|
||||||
- direct SQL callers in controllers/executors.
|
|
||||||
|
|
||||||
## Refresh checklist (read-only)
|
|
||||||
|
|
||||||
Run these metadata checks before/after DB migration:
|
|
||||||
|
|
||||||
1. Column inventory per table (`INFORMATION_SCHEMA.COLUMNS`).
|
|
||||||
2. Constraint inventory (`INFORMATION_SCHEMA.TABLE_CONSTRAINTS` + `KEY_COLUMN_USAGE`).
|
|
||||||
3. Trigger inventory (`sys.triggers`) and definitions (`OBJECT_DEFINITION`).
|
|
||||||
4. Stored procedure inventory (`sys.procedures`) and definitions (`OBJECT_DEFINITION`).
|
|
||||||
5. Repo call-site grep for `PRSIG_` names to detect dead or missing integration points.
|
|
||||||
17
EnvelopeGenerator.API/AuthScheme.cs
Normal file
17
EnvelopeGenerator.API/AuthScheme.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace EnvelopeGenerator.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static class AuthScheme
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheme name used for per-envelope receiver JWT authentication.
|
||||||
|
/// </summary>
|
||||||
|
public const string Receiver = "EnvelopeGenerator.API.ReceiverJWT";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scheme name used for per-envelope sender JWT authentication.
|
||||||
|
/// </summary>
|
||||||
|
public const string Sender = "EnvelopeGenerator.API.SenderJWT";
|
||||||
|
}
|
||||||
132
EnvelopeGenerator.API/Controllers/AnnotationController.cs
Normal file
132
EnvelopeGenerator.API/Controllers/AnnotationController.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.DTO;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
|
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Queries;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages annotations and signature lifecycle for envelopes.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AnnotationController : ControllerBase
|
||||||
|
{
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
private readonly IEnvelopeHistoryService _historyService;
|
||||||
|
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||||
|
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
private readonly ILogger<AnnotationController> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="AnnotationController"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
public AnnotationController(
|
||||||
|
ILogger<AnnotationController> logger,
|
||||||
|
IEnvelopeHistoryService envelopeHistoryService,
|
||||||
|
IEnvelopeReceiverService envelopeReceiverService,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_historyService = envelopeHistoryService;
|
||||||
|
_envelopeReceiverService = envelopeReceiverService;
|
||||||
|
_mediator = mediator;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates or updates annotations for the authenticated envelope receiver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
|
||||||
|
/// <param name="cancel">Cancellation token.</param>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost]
|
||||||
|
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||||
|
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var signature = User.ReceiverSignature();
|
||||||
|
var uuid = User.EnvelopeUuid();
|
||||||
|
|
||||||
|
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
||||||
|
|
||||||
|
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
||||||
|
return Problem(statusCode: StatusCodes.Status409Conflict);
|
||||||
|
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||||
|
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||||
|
|
||||||
|
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
||||||
|
var docSignedNotification = envelopeReceiverDto is not null
|
||||||
|
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
||||||
|
: throw new NotFoundException("Envelope receiver is not found.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _mediator.Publish(docSignedNotification, cancel);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _mediator.Publish(new RemoveSignatureNotification()
|
||||||
|
{
|
||||||
|
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
|
||||||
|
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
|
||||||
|
}, cancel);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rejects the document for the current receiver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">Optional rejection reason.</param>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("reject")]
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||||
|
{
|
||||||
|
var signature = User.ReceiverSignature();
|
||||||
|
var uuid = User.EnvelopeUuid();
|
||||||
|
var mail = User.ReceiverMail();
|
||||||
|
|
||||||
|
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||||
|
|
||||||
|
if (envRcvRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(envRcvRes.Notices);
|
||||||
|
return Unauthorized("you are not authorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
|
||||||
|
if (histRes.IsSuccess)
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
|
||||||
|
_logger.LogNotice(histRes.Notices);
|
||||||
|
return StatusCode(500, histRes.Messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
EnvelopeGenerator.API/Controllers/AuthController.cs
Normal file
117
EnvelopeGenerator.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
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(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[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(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
public IActionResult Check(string? role = null)
|
||||||
|
=> role is not null && !User.IsInRole(role)
|
||||||
|
? Unauthorized()
|
||||||
|
: Ok();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key.
|
||||||
|
/// The request must carry a cookie named <c>AuthTokenSignFLOWReceiver.{envelopeKey}</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey">The unique envelope key extracted from the route.</param>
|
||||||
|
/// <response code="200">Valid per-envelope token found.</response>
|
||||||
|
/// <response code="401">Token is missing, expired or invalid.</response>
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("check/envelope/{envelopeKey}")]
|
||||||
|
public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the per-envelope receiver cookie for the given envelope key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey">The unique envelope key whose cookie should be deleted.</param>
|
||||||
|
/// <response code="200">Cookie successfully deleted.</response>
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
|
[HttpPost("logout/envelope/{envelopeKey}")]
|
||||||
|
public IActionResult LogoutEnvelopeReceiver([FromRoute] string envelopeKey)
|
||||||
|
{
|
||||||
|
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||||
|
Response.Cookies.Delete(cookieName);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all per-envelope receiver cookies from the current request.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">All envelope receiver cookies successfully deleted.</response>
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
|
[HttpPost("logout/envelope")]
|
||||||
|
public IActionResult LogoutAllEnvelopeReceivers()
|
||||||
|
{
|
||||||
|
foreach (var cookieName in Request.Cookies.Keys.Where(k => CookieNames.IsEnvelopeReceiverCookie(k, authTokenKeys.Cookie)))
|
||||||
|
Response.Cookies.Delete(cookieName);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal file
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.API.Options;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages cached data for receivers using distributed cache.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
public class CacheController(
|
||||||
|
IDistributedCache cache,
|
||||||
|
IOptions<CacheOptions> cacheOptions) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a receiver's signature in cache for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> SaveSignature(
|
||||||
|
[FromRoute] string envelopeKey,
|
||||||
|
[FromBody] SignatureCacheRequest request,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
var json = JsonSerializer.Serialize(request);
|
||||||
|
|
||||||
|
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
|
||||||
|
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await cache.SetStringAsync(cacheKey, json, options ?? new DistributedCacheEntryOptions(), cancel);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a cached signature for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> GetSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
var json = await cache.GetStringAsync(cacheKey, cancel);
|
||||||
|
|
||||||
|
if (json is null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var signature = JsonSerializer.Deserialize<SignatureCacheRequest>(json);
|
||||||
|
return Ok(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a cached signature for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpDelete("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
await cache.RemoveAsync(cacheKey, cancel);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for caching signature data.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SignatureCacheRequest(
|
||||||
|
string DataUrl,
|
||||||
|
string FullName,
|
||||||
|
string Place,
|
||||||
|
string? Position = null);
|
||||||
31
EnvelopeGenerator.API/Controllers/ConfigController.cs
Normal file
31
EnvelopeGenerator.API/Controllers/ConfigController.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exposes configuration data required by the client applications.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of <see cref="ConfigController"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||||
|
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns annotation configuration that was previously rendered by MVC.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("Annotations")]
|
||||||
|
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||||
|
public IActionResult GetAnnotationParams()
|
||||||
|
{
|
||||||
|
return Ok(_annotationParams.AnnotationJSObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class DocReceiverElementController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DocReceiverElementController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: update to use signature query
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
int envelopeId = User.EnvelopeId();
|
||||||
|
|
||||||
|
int receiverId = User.ReceiverId();
|
||||||
|
|
||||||
|
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||||
|
|
||||||
|
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
|
||||||
|
return NotFound("Document is empty.");
|
||||||
|
|
||||||
|
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
|
||||||
|
|
||||||
|
if (rcvSignatures is null)
|
||||||
|
return NotFound("No signatures found for the current receiver.");
|
||||||
|
else
|
||||||
|
return Ok(rcvSignatures);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
EnvelopeGenerator.API/Controllers/DocumentController.cs
Normal file
84
EnvelopeGenerator.API/Controllers/DocumentController.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
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>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger<DocumentController> logger) : 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.EnvelopeId();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the document for the specified envelope key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
int envelopeId = User.EnvelopeId();
|
||||||
|
|
||||||
|
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||||
|
|
||||||
|
if (senderDoc.ByteData is not byte[] senderDocByte)
|
||||||
|
return NotFound("Document is empty.");
|
||||||
|
|
||||||
|
Response.Headers.ContentDisposition = $"inline; filename=\"{envelopeKey}.pdf\"";
|
||||||
|
return File(senderDocByte, "application/pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
69
EnvelopeGenerator.API/Controllers/EmailTemplateController.cs
Normal file
69
EnvelopeGenerator.API/Controllers/EmailTemplateController.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.EmailTemplates.Commands;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Application.EmailTemplates.Queries;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for managing temp templates.
|
||||||
|
/// Steuerung zur Verwaltung von E-Mail-Vorlagen.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="mediator">
|
||||||
|
/// Die Mediator-Instanz, die zum Senden von Befehlen und Abfragen verwendet wird.
|
||||||
|
/// </param>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = AuthPolicy.Sender)]
|
||||||
|
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft E-Mail-Vorlagen basierend auf der angegebenen Abfrage ab.
|
||||||
|
/// Gibt alles zurück, wenn keine Id- oder Typ-Informationen eingegeben wurden.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="emailTemplate">Die Abfrageparameter zum Abrufen von E-Mail-Vorlagen.</param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns>Gibt HTTP-Antwort zurück</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sample request:
|
||||||
|
/// GET /api/EmailTemplate?emailTemplateId=123
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="200">Wenn die E-Mail-Vorlagen erfolgreich abgerufen werden.</response>
|
||||||
|
/// <response code="400">Wenn die Abfrageparameter ungültig sind.</response>
|
||||||
|
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
|
||||||
|
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] ReadEmailTemplateQuery emailTemplate, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(emailTemplate, cancel);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an temp template or resets it if no update command is provided.
|
||||||
|
/// Aktualisiert eine E-Mail-Vorlage oder setzt sie zurück, wenn kein Aktualisierungsbefehl angegeben ist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <response code="200">Wenn die E-Mail-Vorlage erfolgreich aktualisiert oder zurückgesetzt wird.</response>
|
||||||
|
/// <response code="400">Wenn die Abfrage ohne einen String gesendet wird.</response>
|
||||||
|
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
|
||||||
|
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
|
||||||
|
[HttpPut]
|
||||||
|
public async Task<IActionResult> Update([FromBody] UpdateEmailTemplateCommand update, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await mediator.Send(update, cancel);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
111
EnvelopeGenerator.API/Controllers/EnvelopeController.cs
Normal file
111
EnvelopeGenerator.API/Controllers/EnvelopeController.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Die API ermöglicht das Abrufen und Verwalten von Umschlägen basierend auf Benutzerinformationen und Statusfiltern.
|
||||||
|
///
|
||||||
|
/// Mögliche Antworten:
|
||||||
|
/// - 200 OK: Die Anfrage war erfolgreich, und die angeforderten Daten werden zurückgegeben.
|
||||||
|
/// - 400 Bad Request: Die Anfrage war fehlerhaft oder unvollständig.
|
||||||
|
/// - 401 Unauthorized: Der Benutzer ist nicht authentifiziert.
|
||||||
|
/// - 403 Forbidden: Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.
|
||||||
|
/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden.
|
||||||
|
/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class EnvelopeController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<EnvelopeController> _logger;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelope"></param>
|
||||||
|
/// <returns>Eine IActionResult-Instanz, die die abgerufenen Umschläge oder einen Fehlerstatus enthält.</returns>
|
||||||
|
/// <response code="200">Die Anfrage war erfolgreich, und die Umschläge werden zurückgegeben.</response>
|
||||||
|
/// <response code="400">Die Anfrage war fehlerhaft oder unvollständig.</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="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
|
||||||
|
return result.Any() ? Ok(result) : NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query"></param>
|
||||||
|
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
|
||||||
|
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
|
||||||
|
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
|
||||||
|
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[HttpGet("doc-result")]
|
||||||
|
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
||||||
|
{
|
||||||
|
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
|
||||||
|
var envelope = envelopes.FirstOrDefault();
|
||||||
|
|
||||||
|
if (envelope is null)
|
||||||
|
return NotFound("Envelope not available.");
|
||||||
|
if (envelope.DocResult is null)
|
||||||
|
return NotFound("The document has not been fully signed or the result has not yet been released.");
|
||||||
|
|
||||||
|
if (view)
|
||||||
|
{
|
||||||
|
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
|
||||||
|
return File(envelope.DocResult, "application/pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[NonAction]
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||||
|
{
|
||||||
|
var res = await _mediator.Send(command.WithAuth(User.GetId()));
|
||||||
|
|
||||||
|
if (res is null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
281
EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs
Normal file
281
EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.API.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Data;
|
||||||
|
using EnvelopeGenerator.Application.Common.SQL;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller für die Verwaltung von Umschlagempfängern.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class EnvelopeReceiverController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<EnvelopeReceiverController> _logger;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IEnvelopeExecutor _envelopeExecutor;
|
||||||
|
private readonly IEnvelopeReceiverExecutor _erExecutor;
|
||||||
|
private readonly IDocumentExecutor _documentExecutor;
|
||||||
|
private readonly string _cnnStr;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Konstruktor für den EnvelopeReceiverController.
|
||||||
|
/// </summary>
|
||||||
|
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_mapper = mapper;
|
||||||
|
_envelopeExecutor = envelopeExecutor;
|
||||||
|
_erExecutor = erExecutor;
|
||||||
|
_documentExecutor = documentExecutor;
|
||||||
|
_cnnStr = csOpt.Value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeReceiver">Die Abfrageparameter für die Filterung von Umschlagempfängern.</param>
|
||||||
|
/// <returns>Eine HTTP-Antwort mit der Liste der gefundenen Umschlagempfänger oder einem Fehlerstatus.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Endpunkt ermöglicht es, Umschlagempfänger basierend auf dem Benutzernamen und optionalen Statusfiltern abzurufen.
|
||||||
|
/// Wenn der Benutzername nicht ermittelt werden kann, wird ein Serverfehler zurückgegeben.
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="200">Die Liste der Umschlagempfänger wurde erfolgreich abgerufen.</response>
|
||||||
|
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
|
||||||
|
{
|
||||||
|
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
|
||||||
|
|
||||||
|
var result = await _mediator.Send(envelopeReceiver);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var er = await _mediator.Send(new ReadEnvelopeReceiverQuery()
|
||||||
|
{
|
||||||
|
Key = envelopeKey
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
return Ok(er.SingleOrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="receiver">Abfrage, bei der nur eine der Angaben ID, Signatur oder E-Mail-Adresse des Empfängers eingegeben werden muss.</param>
|
||||||
|
/// <returns>Eine HTTP-Antwort mit dem Namen des Empfängers oder einem Fehlerstatus.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Endpunkt ermöglicht es, den Namen des zuletzt verwendeten Empfängers basierend auf der E-Mail-Adresse abzurufen.
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="200">Der Name des Empfängers wurde erfolgreich abgerufen.</response>
|
||||||
|
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||||
|
/// <response code="404">Kein Empfänger gefunden.</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("salute")]
|
||||||
|
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
||||||
|
{
|
||||||
|
var name = await _mediator.Send(receiver);
|
||||||
|
return name is null ? NotFound() : Ok(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns>HTTP-Antwort</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sample request:
|
||||||
|
///
|
||||||
|
/// POST /api/envelope
|
||||||
|
/// {
|
||||||
|
/// "title": "Vertragsdokument",
|
||||||
|
/// "message": "Bitte unterschreiben Sie dieses Dokument.",
|
||||||
|
/// "document": {
|
||||||
|
/// "dataAsBase64": "dGVzdC1iYXNlNjQtZGF0YQ=="
|
||||||
|
/// },
|
||||||
|
/// "receivers": [
|
||||||
|
/// {
|
||||||
|
/// "emailAddress": "example@example.com",
|
||||||
|
/// "signatures": [
|
||||||
|
/// {
|
||||||
|
/// "x": 100,
|
||||||
|
/// "y": 200,
|
||||||
|
/// "page": 1
|
||||||
|
/// }
|
||||||
|
/// ],
|
||||||
|
/// "name": "Max Mustermann",
|
||||||
|
/// "phoneNumber": "+49123456789"
|
||||||
|
/// }
|
||||||
|
/// ],
|
||||||
|
/// "tfaEnabled": false
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="202">Envelope-Erstellung und Sendeprozessbefehl erfolgreich</response>
|
||||||
|
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
||||||
|
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||||
|
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
#region Create Envelope
|
||||||
|
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Add receivers
|
||||||
|
List<EnvelopeReceiver> sentReceivers = new();
|
||||||
|
List<ReceiverGetOrCreateCommand> unsentReceivers = new();
|
||||||
|
|
||||||
|
foreach (var receiver in request.Receivers)
|
||||||
|
{
|
||||||
|
var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
||||||
|
|
||||||
|
if (envelopeReceiver is null)
|
||||||
|
unsentReceivers.Add(receiver);
|
||||||
|
else
|
||||||
|
sentReceivers.Add(envelopeReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = _mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
||||||
|
res.UnsentReceivers = unsentReceivers;
|
||||||
|
res.SentReceiver = _mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Add document
|
||||||
|
var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
||||||
|
|
||||||
|
if (document is null)
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Add document element
|
||||||
|
// @DOC_ID, @RECEIVER_ID, @POSITION_X, @POSITION_Y, @PAGE
|
||||||
|
string sql = @"
|
||||||
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
|
EXEC [dbo].[PRSIG_API_ADD_DOC_RECEIVER_ELEM]
|
||||||
|
{0},
|
||||||
|
{1},
|
||||||
|
{2},
|
||||||
|
{3},
|
||||||
|
{4},
|
||||||
|
@OUT_SUCCESS OUTPUT;
|
||||||
|
|
||||||
|
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||||
|
|
||||||
|
foreach (var rcv in res.SentReceiver)
|
||||||
|
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.DocReceiverElementCreateDto>())
|
||||||
|
{
|
||||||
|
using SqlConnection conn = new(_cnnStr);
|
||||||
|
conn.Open();
|
||||||
|
|
||||||
|
var formattedSQL = string.Format(sql, document.Id.ToSqlParam(), rcv.Id.ToSqlParam(), sign.X.ToSqlParam(), sign.Y.ToSqlParam(), sign.Page.ToSqlParam());
|
||||||
|
|
||||||
|
using SqlCommand cmd = new(formattedSQL, conn);
|
||||||
|
cmd.CommandType = CommandType.Text;
|
||||||
|
|
||||||
|
using SqlDataReader reader = cmd.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
|
document.Id, rcv.Id, sign.Page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Create history
|
||||||
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
|
string sql_hist = @"
|
||||||
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
|
{0},
|
||||||
|
{1},
|
||||||
|
{2},
|
||||||
|
@OUT_SUCCESS OUTPUT;
|
||||||
|
|
||||||
|
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||||
|
|
||||||
|
using (SqlConnection conn = new(_cnnStr))
|
||||||
|
{
|
||||||
|
conn.Open();
|
||||||
|
var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), User.GetId().ToSqlParam());
|
||||||
|
using SqlCommand cmd = new(formattedSQL_hist, conn);
|
||||||
|
cmd.CommandType = CommandType.Text;
|
||||||
|
|
||||||
|
using SqlDataReader reader = cmd.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
|
envelope.Uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool IsBase64String(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Convert.FromBase64String(input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
EnvelopeGenerator.API/Controllers/EnvelopeTypeController.cs
Normal file
39
EnvelopeGenerator.API/Controllers/EnvelopeTypeController.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class EnvelopeTypeController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<EnvelopeTypeController> _logger;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IMediator mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAllAsync()
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
EnvelopeGenerator.API/Controllers/HistoryController.cs
Normal file
118
EnvelopeGenerator.API/Controllers/HistoryController.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Queries;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class HistoryController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Konstruktor für den HistoryController.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="memoryCache"></param>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public HistoryController(IMemoryCache memoryCache, IMediator mediator)
|
||||||
|
{
|
||||||
|
_memoryCache = memoryCache;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt alle möglichen Verweise auf alle möglichen Include in einem Verlaufsdatensatz zurück. (z. B. DocumentSigned bezieht sich auf Receiver.)
|
||||||
|
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
||||||
|
/// 1 - Sender:
|
||||||
|
/// Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 1* beginnen.
|
||||||
|
/// 2 - Receiver:
|
||||||
|
/// Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 2* beginnen.
|
||||||
|
/// 3 - System:
|
||||||
|
/// Historische Datensätze, die sich auf den allgemeinen Zustand des Umschlags beziehen. Diese haben Statuscodes, die mit 3* beginnen.
|
||||||
|
/// 4 - Unknown:
|
||||||
|
/// Ein unbekannter Datensatz weist auf einen möglichen Mangel oder eine Unstimmigkeit im Aktualisierungsprozess der Anwendung hin.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
[HttpGet("related")]
|
||||||
|
[Authorize]
|
||||||
|
public IActionResult GetReferenceTypes(ReferenceType? referenceType = null)
|
||||||
|
{
|
||||||
|
return referenceType is null
|
||||||
|
? Ok(_memoryCache.GetEnumAsDictionary<ReferenceType>("gen.api", ReferenceType.Unknown))
|
||||||
|
: Ok(referenceType.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt alle möglichen Include in einem Verlaufsdatensatz zurück.
|
||||||
|
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
||||||
|
/// 1003: EnvelopeQueued
|
||||||
|
/// 1006: EnvelopeCompletelySigned
|
||||||
|
/// 1007: EnvelopeReportCreated
|
||||||
|
/// 1008: EnvelopeArchived
|
||||||
|
/// 1009: EnvelopeDeleted
|
||||||
|
/// 10007: EnvelopeRejected
|
||||||
|
/// 10009: EnvelopeWithdrawn
|
||||||
|
/// 2001: AccessCodeRequested
|
||||||
|
/// 2002: AccessCodeCorrect
|
||||||
|
/// 2003: AccessCodeIncorrect
|
||||||
|
/// 2004: DocumentOpened
|
||||||
|
/// 2005: DocumentSigned
|
||||||
|
/// 2006: DocumentForwarded
|
||||||
|
/// 2007: DocumentRejected
|
||||||
|
/// 2008: EnvelopeShared
|
||||||
|
/// 2009: EnvelopeViewed
|
||||||
|
/// 3001: MessageInvitationSent (Wird von Trigger verwendet)
|
||||||
|
/// 3002: MessageAccessCodeSent
|
||||||
|
/// 3003: MessageConfirmationSent
|
||||||
|
/// 3004: MessageDeletionSent
|
||||||
|
/// 3005: MessageCompletionSent
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="status">
|
||||||
|
/// Abfrageparameter, der angibt, auf welche Referenz sich der Include bezieht.
|
||||||
|
/// 1 - Sender: Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 1* beginnen.
|
||||||
|
/// 2 - Receiver: Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 2* beginnen.
|
||||||
|
/// 3 - System: Diese werden durch Datenbank-Trigger aktualisiert und sind in den Tabellen EnvelopeHistory und EmailOut zu finden.Sie arbeiten
|
||||||
|
/// integriert mit der Anwendung EmailProfiler, um E-Mails zu versenden und haben die Codes, die mit 3* beginnen.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>Gibt die HTTP-Antwort zurück.</returns>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
[HttpGet("status")]
|
||||||
|
[Authorize]
|
||||||
|
public IActionResult GetEnvelopeStatus([FromQuery] EnvelopeStatus? status = null)
|
||||||
|
{
|
||||||
|
return status is null
|
||||||
|
? Ok(_memoryCache.GetEnumAsDictionary<EnvelopeStatus>("gen.api", Status.NonHist, Status.RelatedToFormApp))
|
||||||
|
: Ok(status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft die gesamte Umschlaghistorie basierend auf den angegebenen Abfrageparametern ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="historyQuery">Die Abfrageparameter, die die Filterkriterien für die Umschlaghistorie definieren.</param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns>Eine Liste von Historieneinträgen, die den angegebenen Kriterien entsprechen, oder nur der letzte Eintrag.</returns>
|
||||||
|
/// <response code="200">Die Anfrage war erfolgreich, und die Umschlaghistorie wird zurückgegeben.</response>
|
||||||
|
/// <response code="400">Die Anfrage war ungültig oder unvollständig.</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="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> GetAllAsync([FromQuery] ReadHistoryQuery historyQuery, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var history = await _mediator.Send(historyQuery, cancel).ThrowIfEmpty(Exceptions.NotFound);
|
||||||
|
return Ok((historyQuery.OnlyLast) ? history.MaxBy(h => h.AddedWhen) : history);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuthController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
IAuthorizationService AuthService { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
ClaimsPrincipal User { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static class AuthControllerExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="controller"></param>
|
||||||
|
/// <param name="policyName"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<bool> IsUserInPolicyAsync(this IAuthController controller, string policyName)
|
||||||
|
{
|
||||||
|
var result = await controller.AuthService.AuthorizeAsync(controller.User, policyName);
|
||||||
|
return result.Succeeded;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
EnvelopeGenerator.API/Controllers/LocalizationController.cs
Normal file
121
EnvelopeGenerator.API/Controllers/LocalizationController.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using DigitalData.Core.API;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
|
||||||
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class LocalizationController : ControllerBase
|
||||||
|
{
|
||||||
|
private static readonly Guid L_KEY = Guid.NewGuid();
|
||||||
|
|
||||||
|
private readonly ILogger<LocalizationController> _logger;
|
||||||
|
private readonly IStringLocalizer<Resource> _mLocalizer;
|
||||||
|
private readonly IStringLocalizer<Resource> _localizer;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Konstruktor für den <see cref="LocalizationController"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger für die Protokollierung.</param>
|
||||||
|
/// <param name="localizer">Lokalisierungsdienst für Ressourcen.</param>
|
||||||
|
/// <param name="memoryCache">Speicher-Cache für die Zwischenspeicherung von Daten.</param>
|
||||||
|
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
|
||||||
|
public LocalizationController(
|
||||||
|
ILogger<LocalizationController> logger,
|
||||||
|
IStringLocalizer<Resource> localizer,
|
||||||
|
IMemoryCache memoryCache,
|
||||||
|
IStringLocalizer<Resource> _modelLocalizer)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_localizer = localizer;
|
||||||
|
_cache = memoryCache;
|
||||||
|
_mLocalizer = _modelLocalizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft alle lokalisierten Daten ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Eine Liste aller lokalisierten Daten.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult GetAll() => Ok(_cache.GetOrCreate(Language ?? string.Empty + L_KEY, _ => _mLocalizer.ToDictionary()));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft die aktuelle Sprache ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Die aktuelle Sprache oder ein NotFound-Ergebnis, wenn keine Sprache gesetzt ist.</returns>
|
||||||
|
[HttpGet("lang")]
|
||||||
|
public IActionResult GetLanguage() => Language is null ? NotFound() : Ok(Language);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Setzt die Sprache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">Die zu setzende Sprache.</param>
|
||||||
|
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gesetzt wurde, oder ein BadRequest-Ergebnis, wenn die Eingabe ungültig ist.</returns>
|
||||||
|
[HttpPost("lang")]
|
||||||
|
public IActionResult SetLanguage([FromQuery] string language)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(language))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
Language = language;
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Löscht die aktuelle Sprache.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gelöscht wurde.</returns>
|
||||||
|
[HttpDelete("lang")]
|
||||||
|
public IActionResult DeleteLanguage()
|
||||||
|
{
|
||||||
|
Language = null;
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eigenschaft für die Verwaltung der aktuellen Sprache über Cookies.
|
||||||
|
/// </summary>
|
||||||
|
private string? Language
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(cookieValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
|
||||||
|
return culture?.Value ?? null;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var cookieOptions = new CookieOptions()
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddYears(1),
|
||||||
|
Secure = false,
|
||||||
|
SameSite = SameSiteMode.Strict,
|
||||||
|
HttpOnly = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Response.Cookies.Append(
|
||||||
|
CookieRequestCultureProvider.DefaultCookieName,
|
||||||
|
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)),
|
||||||
|
cookieOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
EnvelopeGenerator.API/Controllers/ReadOnlyController.cs
Normal file
91
EnvelopeGenerator.API/Controllers/ReadOnlyController.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.DTO;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages read-only envelope sharing flows.
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class ReadOnlyController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<ReadOnlyController> _logger;
|
||||||
|
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
|
||||||
|
private readonly IEnvelopeMailService _mailService;
|
||||||
|
private readonly IEnvelopeHistoryService _historyService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ReadOnlyController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyController(ILogger<ReadOnlyController> logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_readOnlyService = readOnlyService;
|
||||||
|
_mailService = mailService;
|
||||||
|
_historyService = historyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new read-only receiver for the current envelope.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="createDto">Creation payload.</param>
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||||
|
{
|
||||||
|
var authReceiverMail = User.ReceiverMail();
|
||||||
|
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));
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelopeId = User.EnvelopeId();
|
||||||
|
|
||||||
|
createDto.AddedWho = authReceiverMail;
|
||||||
|
createDto.EnvelopeId = envelopeId;
|
||||||
|
|
||||||
|
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
|
||||||
|
|
||||||
|
if (creationRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(creationRes);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id);
|
||||||
|
if (readRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(creationRes);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newReadOnly = readRes.Data;
|
||||||
|
|
||||||
|
return await _mailService.SendAsync(newReadOnly).ThenAsync<int, IActionResult>(SuccessAsync: async _ =>
|
||||||
|
{
|
||||||
|
var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared);
|
||||||
|
if (histRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto));
|
||||||
|
_logger.LogNotice(histRes.Notices);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
},
|
||||||
|
|
||||||
|
Fail: (msg, ntc) =>
|
||||||
|
{
|
||||||
|
_logger.LogNotice(ntc);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
47
EnvelopeGenerator.API/Controllers/ReceiverController.cs
Normal file
47
EnvelopeGenerator.API/Controllers/ReceiverController.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller für die Verwaltung von Empfängern.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class ReceiverController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mediator">Mediator für Anfragen.</param>
|
||||||
|
public ReceiverController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft eine Liste von Empfängern ab, basierend auf den angegebenen Abfrageparametern.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
||||||
|
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
||||||
|
{
|
||||||
|
if (!receiver.HasAnyCriteria)
|
||||||
|
{
|
||||||
|
var all = await _mediator.Send(new ReadReceiverQuery());
|
||||||
|
return Ok(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _mediator.Send(receiver);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
EnvelopeGenerator.API/Controllers/TfaRegistrationController.cs
Normal file
129
EnvelopeGenerator.API/Controllers/TfaRegistrationController.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.DTO;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Models;
|
||||||
|
using Ganss.Xss;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/tfa")]
|
||||||
|
public class TfaRegistrationController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<TfaRegistrationController> _logger;
|
||||||
|
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||||
|
private readonly IAuthenticator _authenticator;
|
||||||
|
private readonly IReceiverService _receiverService;
|
||||||
|
private readonly TFARegParams _parameters;
|
||||||
|
private readonly IStringLocalizer<Resource> _localizer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TfaRegistrationController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public TfaRegistrationController(
|
||||||
|
ILogger<TfaRegistrationController> logger,
|
||||||
|
IEnvelopeReceiverService envelopeReceiverService,
|
||||||
|
IAuthenticator authenticator,
|
||||||
|
IReceiverService receiverService,
|
||||||
|
IOptions<TFARegParams> tfaRegParamsOptions,
|
||||||
|
IStringLocalizer<Resource> localizer)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_envelopeReceiverService = envelopeReceiverService;
|
||||||
|
_authenticator = authenticator;
|
||||||
|
_receiverService = receiverService;
|
||||||
|
_parameters = tfaRegParamsOptions.Value;
|
||||||
|
_localizer = localizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates registration metadata (QR code and deadline) for a receiver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeReceiverId">Encoded envelope receiver id.</param>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("{envelopeReceiverId}")]
|
||||||
|
public async Task<IActionResult> RegisterAsync(string envelopeReceiverId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (uuid, signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
|
||||||
|
|
||||||
|
if (uuid is null || signature is null)
|
||||||
|
{
|
||||||
|
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
|
||||||
|
return Unauthorized(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretResult = await _envelopeReceiverService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||||
|
if (secretResult.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(secretResult.Notices);
|
||||||
|
return NotFound(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelopeReceiver = secretResult.Data;
|
||||||
|
|
||||||
|
if (!envelopeReceiver.Envelope!.TFAEnabled)
|
||||||
|
return Unauthorized(new { message = _localizer.WrongAccessCode() });
|
||||||
|
|
||||||
|
var receiver = envelopeReceiver.Receiver;
|
||||||
|
receiver!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
|
||||||
|
await _receiverService.UpdateAsync(receiver);
|
||||||
|
var totpQr64 = _authenticator.GenerateTotpQrCode(userEmail: receiver.EmailAddress, secretKey: receiver.TotpSecretkey).ToBase64String();
|
||||||
|
|
||||||
|
if (receiver.TfaRegDeadline is null)
|
||||||
|
{
|
||||||
|
receiver.TfaRegDeadline = _parameters.Deadline;
|
||||||
|
await _receiverService.UpdateAsync(receiver);
|
||||||
|
}
|
||||||
|
else if (receiver.TfaRegDeadline <= DateTime.Now)
|
||||||
|
{
|
||||||
|
return StatusCode(StatusCodes.Status410Gone, new { message = _localizer.WrongAccessCode() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
envelopeReceiver.EnvelopeId,
|
||||||
|
envelopeReceiver.Envelope!.Uuid,
|
||||||
|
envelopeReceiver.Receiver!.Signature,
|
||||||
|
receiver.TfaRegDeadline,
|
||||||
|
TotpQR64 = totpQr64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer.WrongEnvelopeReceiverId());
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs out the envelope receiver from cookie authentication.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("auth/logout")]
|
||||||
|
public async Task<IActionResult> LogOutAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{message}", ex.Message);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
EnvelopeGenerator.API/Documentation/AuthProxyDocumentFilter.cs
Normal file
123
EnvelopeGenerator.API/Documentation/AuthProxyDocumentFilter.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using EnvelopeGenerator.API.Models;
|
||||||
|
using Microsoft.OpenApi.Any;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Documentation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuthProxyDocumentFilter : IDocumentFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="swaggerDoc"></param>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||||
|
{
|
||||||
|
AddLoginOperation(swaggerDoc, context);
|
||||||
|
AddEnvelopeReceiverLoginOperation(swaggerDoc, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||||
|
{
|
||||||
|
const string path = "/api/auth";
|
||||||
|
|
||||||
|
var loginSchema = context.SchemaGenerator.GenerateSchema(typeof(Login), context.SchemaRepository);
|
||||||
|
var loginExample = new OpenApiObject
|
||||||
|
{
|
||||||
|
["password"] = new OpenApiString(""),
|
||||||
|
["username"] = new OpenApiString("")
|
||||||
|
};
|
||||||
|
|
||||||
|
var operation = new OpenApiOperation
|
||||||
|
{
|
||||||
|
Summary = "Proxy login (auth-hub)",
|
||||||
|
Description = "Proxies the request to the auth service. Add query parameter `cookie=true|false`.",
|
||||||
|
Tags = [new() { Name = "Auth" }],
|
||||||
|
Parameters =
|
||||||
|
{
|
||||||
|
new OpenApiParameter
|
||||||
|
{
|
||||||
|
Name = "cookie",
|
||||||
|
In = ParameterLocation.Query,
|
||||||
|
Required = false,
|
||||||
|
Schema = new OpenApiSchema { Type = "boolean", Default = new OpenApiBoolean(true) },
|
||||||
|
Example = new OpenApiBoolean(true),
|
||||||
|
Description = "If true, auth service sets the auth cookie."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RequestBody = new OpenApiRequestBody
|
||||||
|
{
|
||||||
|
Required = true,
|
||||||
|
Content =
|
||||||
|
{
|
||||||
|
["application/json"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample },
|
||||||
|
["multipart/form-data"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Responses =
|
||||||
|
{
|
||||||
|
["200"] = new OpenApiResponse { Description = "OK (proxied response)" },
|
||||||
|
["401"] = new OpenApiResponse { Description = "Unauthorized" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
swaggerDoc.Paths[path] = new OpenApiPathItem
|
||||||
|
{
|
||||||
|
Operations =
|
||||||
|
{
|
||||||
|
[OperationType.Post] = operation
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
84
EnvelopeGenerator.API/EnvelopeGenerator.API.csproj
Normal file
84
EnvelopeGenerator.API/EnvelopeGenerator.API.csproj
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net8.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.4.0</Version>
|
||||||
|
<FileVersion>1.4.0</FileVersion>
|
||||||
|
<AssemblyVersion>1.4.0</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.Claims" Version="1.0.3" />
|
||||||
|
<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.Authentication.JwtBearer" Version="8.0.28" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
|
<PackageReference Include="itext" Version="8.0.5" />
|
||||||
|
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" Condition="'$(TargetFramework)' == 'net9.0'" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
|
<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" />
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
96
EnvelopeGenerator.API/Extensions/ReceiverClaimExtensions.cs
Normal file
96
EnvelopeGenerator.API/Extensions/ReceiverClaimExtensions.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides helper methods for working with envelope-specific authentication claims.
|
||||||
|
/// </summary>
|
||||||
|
public static class ReceiverClaimExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="claimType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
private static string GetRequiredClaimValue(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes)
|
||||||
|
{
|
||||||
|
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
|
||||||
|
{
|
||||||
|
var value = user.FindFirstValue(claimType);
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
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(s) '{string.Join("', '", claimTypes)}' are 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 EnvelopeUuid(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated receiver signature from the claims.
|
||||||
|
/// </summary>
|
||||||
|
public static string ReceiverSignature(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated receiver email address from the claims.
|
||||||
|
/// </summary>
|
||||||
|
public static string ReceiverMail(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated envelope identifier from the claims.
|
||||||
|
/// </summary>
|
||||||
|
public static int EnvelopeId(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId);
|
||||||
|
if (int.TryParse(envIdStr, out var envId))
|
||||||
|
return envId;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated receiver identifier from the claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public static int ReceiverId(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
|
||||||
|
if (int.TryParse(rcvIdStr, out var rcvId))
|
||||||
|
return rcvId;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
95
EnvelopeGenerator.API/Extensions/SenderClaimExtensions.cs
Normal file
95
EnvelopeGenerator.API/Extensions/SenderClaimExtensions.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class SenderClaimExtensions
|
||||||
|
{
|
||||||
|
private static string GetRequiredClaimOfSender(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, params string[] claimTypes)
|
||||||
|
{
|
||||||
|
string? value = null;
|
||||||
|
|
||||||
|
foreach (var claimType in claimTypes)
|
||||||
|
{
|
||||||
|
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 among [{string.Join(", ", claimTypes)}] is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||||
|
/// <returns>The user's ID as an integer.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if the user ID claim is missing or invalid.</exception>
|
||||||
|
public static int GetId(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var idValue = user.GetRequiredClaimOfSender(ClaimTypes.NameIdentifier, "sub");
|
||||||
|
|
||||||
|
if (!int.TryParse(idValue, out var result))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the username from the claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||||
|
/// <returns>The username as a string.</returns>
|
||||||
|
public static string GetUsername(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimOfSender(ClaimTypes.Name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the user's surname (last name) from the claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||||
|
/// <returns>The surname as a string.</returns>
|
||||||
|
public static string GetName(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimOfSender(ClaimTypes.Surname);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the user's given name (first name) from the claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||||
|
/// <returns>The given name as a string.</returns>
|
||||||
|
public static string GetPrename(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimOfSender(ClaimTypes.GivenName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the user's email address from the claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||||
|
/// <returns>The email address as a string.</returns>
|
||||||
|
public static string GetEmail(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimOfSender(ClaimTypes.Email);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
EnvelopeGenerator.API/Jenkinsfile
vendored
Normal file
10
EnvelopeGenerator.API/Jenkinsfile
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
stages {
|
||||||
|
stage('Build') {
|
||||||
|
steps {
|
||||||
|
sh 'dotnet build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Middleware;
|
||||||
|
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware for handling exceptions globally in the application.
|
||||||
|
/// Captures exceptions thrown during the request pipeline execution,
|
||||||
|
/// logs them, and returns an appropriate HTTP response with a JSON error message.
|
||||||
|
/// </summary>
|
||||||
|
public class ExceptionHandlingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">The next middleware in the request pipeline.</param>
|
||||||
|
/// <param name="logger">The logger instance for logging exceptions.</param>
|
||||||
|
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the middleware to handle the HTTP request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The HTTP context of the current request.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context); // Continue down the pipeline
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await HandleExceptionAsync(context, ex, _logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles exceptions by logging them and writing an appropriate JSON response.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The HTTP context of the current request.</param>
|
||||||
|
/// <param name="exception">The exception that occurred.</param>
|
||||||
|
/// <param name="logger">The logger instance for logging the exception.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
string message;
|
||||||
|
|
||||||
|
switch (exception)
|
||||||
|
{
|
||||||
|
case BadRequestException badRequestEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
message = badRequestEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotFoundException notFoundEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
message = notFoundEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
message = "An unexpected error occurred.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
14
EnvelopeGenerator.API/Models/Auth.cs
Normal file
14
EnvelopeGenerator.API/Models/Auth.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||||
|
{
|
||||||
|
public bool HasAccessCode => AccessCode is not null;
|
||||||
|
|
||||||
|
public bool HasSmsCode => SmsCode is not null;
|
||||||
|
|
||||||
|
public bool HasAuthenticatorCode => AuthenticatorCode is not null;
|
||||||
|
|
||||||
|
public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1;
|
||||||
|
|
||||||
|
public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode);
|
||||||
|
}
|
||||||
28
EnvelopeGenerator.API/Models/AuthTokenKeys.cs
Normal file
28
EnvelopeGenerator.API/Models/AuthTokenKeys.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the keys and default values used for authentication token handling
|
||||||
|
/// within the Envelope Generator API.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthTokenKeys
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the cookie used to store the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string Cookie { get; init; } = "AuthToken";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the query string parameter used to pass the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string QueryString { get; init; } = "AuthToken";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the expected issuer value for the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string Issuer { get; init; } = "auth.digitaldata.works";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the expected audience value for the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string Audience { get; init; } = "sign-flow.digitaldata.works";
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
60
EnvelopeGenerator.API/Models/ContactLink.cs
Normal file
60
EnvelopeGenerator.API/Models/ContactLink.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a hyperlink for contact purposes with various HTML attributes.
|
||||||
|
/// </summary>
|
||||||
|
public class ContactLink
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the label of the hyperlink.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; init; } = "Contact";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL that the hyperlink points to.
|
||||||
|
/// </summary>
|
||||||
|
public string Href { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the target where the hyperlink should open.
|
||||||
|
/// Commonly used values are "_blank", "_self", "_parent", "_top".
|
||||||
|
/// </summary>
|
||||||
|
public string Target { get; set; } = "_blank";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the relationship of the linked URL as space-separated link types.
|
||||||
|
/// Examples include "nofollow", "noopener", "noreferrer".
|
||||||
|
/// </summary>
|
||||||
|
public string Rel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the filename that should be downloaded when clicking the hyperlink.
|
||||||
|
/// This attribute will only have an effect if the href attribute is set.
|
||||||
|
/// </summary>
|
||||||
|
public string Download { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the language of the linked resource. Useful when linking to
|
||||||
|
/// content in another language.
|
||||||
|
/// </summary>
|
||||||
|
public string HrefLang { get; set; } = "en";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the MIME type of the linked URL. Helps browsers to handle
|
||||||
|
/// the type correctly when the link is clicked.
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets additional information about the hyperlink, typically viewed
|
||||||
|
/// as a tooltip when the mouse hovers over the link.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an identifier for the hyperlink, unique within the HTML document.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
EnvelopeGenerator.API/Models/Culture.cs
Normal file
17
EnvelopeGenerator.API/Models/Culture.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class Culture
|
||||||
|
{
|
||||||
|
private string _language = string.Empty;
|
||||||
|
public string Language { get => _language;
|
||||||
|
init {
|
||||||
|
_language = value;
|
||||||
|
Info = new(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public string FIClass { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public CultureInfo? Info { get; init; }
|
||||||
|
}
|
||||||
12
EnvelopeGenerator.API/Models/Cultures.cs
Normal file
12
EnvelopeGenerator.API/Models/Cultures.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class Cultures : List<Culture>
|
||||||
|
{
|
||||||
|
public IEnumerable<string> Languages => this.Select(c => c.Language);
|
||||||
|
|
||||||
|
public IEnumerable<string> FIClasses => this.Select(c => c.FIClass);
|
||||||
|
|
||||||
|
public Culture Default => this.First();
|
||||||
|
|
||||||
|
public Culture? this[string? language] => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
7
EnvelopeGenerator.API/Models/EnvelopeReceiverLogin.cs
Normal file
7
EnvelopeGenerator.API/Models/EnvelopeReceiverLogin.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request body for the envelope-receiver login endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="AccessCode">The access code sent to the receiver.</param>
|
||||||
|
public record EnvelopeReceiverLogin(string? AccessCode = null);
|
||||||
10
EnvelopeGenerator.API/Models/ErrorViewModel.cs
Normal file
10
EnvelopeGenerator.API/Models/ErrorViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class ErrorViewModel
|
||||||
|
{
|
||||||
|
public string Title { get; init; } = "404";
|
||||||
|
|
||||||
|
public string Subtitle { get; init; } = "Hmmm...";
|
||||||
|
|
||||||
|
public string Body { get; init; } = "It looks like one of the developers fell asleep";
|
||||||
|
}
|
||||||
10
EnvelopeGenerator.API/Models/Image.cs
Normal file
10
EnvelopeGenerator.API/Models/Image.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class Image
|
||||||
|
{
|
||||||
|
public string Src { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public Dictionary<string, string> Classes { get; init; } = new();
|
||||||
|
|
||||||
|
public string GetClassIn(string page) => Classes.TryGetValue(page, out var cls) && cls is not null ? cls : string.Empty;
|
||||||
|
}
|
||||||
13
EnvelopeGenerator.API/Models/Login.cs
Normal file
13
EnvelopeGenerator.API/Models/Login.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Password">Das erforderliche Passwort für das Login.</param>
|
||||||
|
/// <param name="UserId">Die optionale ID des Benutzers.</param>
|
||||||
|
/// <param name="Username">Der optionale Benutzername.</param>
|
||||||
|
public record Login([Required] string Password, int? UserId = null, string? Username = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
6
EnvelopeGenerator.API/Models/MainViewModel.cs
Normal file
6
EnvelopeGenerator.API/Models/MainViewModel.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class MainViewModel
|
||||||
|
{
|
||||||
|
public string? Title { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public record Annotation : IAnnotation
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
|
||||||
|
#region Bound Annotation
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? HorBoundAnnotName { get; init; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? VerBoundAnnotName { get; init; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Layout
|
||||||
|
[JsonIgnore]
|
||||||
|
public double? MarginLeft { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double MarginLeftRatio { get; init; } = 1;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double? MarginTop { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double MarginTopRatio { get; init; } = 1;
|
||||||
|
|
||||||
|
public double? Width { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double WidthRatio { get; init; } = 1;
|
||||||
|
|
||||||
|
public double? Height { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double HeightRatio { get; init; } = 1;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Position
|
||||||
|
public double Left => (MarginLeft ?? 0) + (HorBoundAnnot?.HorBoundary ?? 0);
|
||||||
|
|
||||||
|
public double Top => (MarginTop ?? 0) + (VerBoundAnnot?.VerBoundary ?? 0);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Boundary
|
||||||
|
[JsonIgnore]
|
||||||
|
public double HorBoundary => Left + (Width ?? 0);
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double VerBoundary => Top + (Height ?? 0);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region BoundAnnot
|
||||||
|
[JsonIgnore]
|
||||||
|
public Annotation? HorBoundAnnot { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public Annotation? VerBoundAnnot { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public Color? BackgroundColor { get; init; }
|
||||||
|
|
||||||
|
#region Border
|
||||||
|
public Color? BorderColor { get; init; }
|
||||||
|
|
||||||
|
public string? BorderStyle { get; init; }
|
||||||
|
|
||||||
|
public int? BorderWidth { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
internal Annotation Default
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
// To set null value, annotation must have null (0) value but null must has non-null value
|
||||||
|
if (MarginLeft == null && value.MarginLeft != null)
|
||||||
|
MarginLeft = value.MarginLeft * MarginLeftRatio;
|
||||||
|
|
||||||
|
if (MarginTop == null && value.MarginTop != null)
|
||||||
|
MarginTop = value.MarginTop * MarginTopRatio;
|
||||||
|
|
||||||
|
if (Width == null && value.Width != null)
|
||||||
|
Width = value.Width * WidthRatio;
|
||||||
|
|
||||||
|
if (Height == null && value.Height != null)
|
||||||
|
Height = value.Height * HeightRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public class AnnotationParams
|
||||||
|
{
|
||||||
|
public AnnotationParams()
|
||||||
|
{
|
||||||
|
_AnnotationJSObjectInitor = new(CreateAnnotationJSObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Background? Background { get; init; }
|
||||||
|
|
||||||
|
#region Annotation
|
||||||
|
[JsonIgnore]
|
||||||
|
public Annotation? DefaultAnnotation { get; init; }
|
||||||
|
|
||||||
|
private readonly List<Annotation> _annots = new List<Annotation>();
|
||||||
|
|
||||||
|
public bool TryGet(string name, out Annotation annotation)
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||||
|
annotation = _annots.FirstOrDefault(a => a.Name == name);
|
||||||
|
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||||
|
return annotation is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public required IEnumerable<Annotation> Annotations
|
||||||
|
{
|
||||||
|
get => _annots;
|
||||||
|
init
|
||||||
|
{
|
||||||
|
_annots = value.ToList();
|
||||||
|
|
||||||
|
if (DefaultAnnotation is not null)
|
||||||
|
foreach (var annot in _annots)
|
||||||
|
annot.Default = DefaultAnnotation;
|
||||||
|
|
||||||
|
for (int i = 0; i < _annots.Count; i++)
|
||||||
|
{
|
||||||
|
#region set bound annotations
|
||||||
|
// horizontal
|
||||||
|
if (_annots[i].HorBoundAnnotName is string horBoundAnnotName)
|
||||||
|
if (TryGet(horBoundAnnotName, out var horBoundAnnot))
|
||||||
|
_annots[i].HorBoundAnnot = horBoundAnnot;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||||
|
|
||||||
|
// vertical
|
||||||
|
if (_annots[i].VerBoundAnnotName is string verBoundAnnotName)
|
||||||
|
if (TryGet(verBoundAnnotName, out var verBoundAnnot))
|
||||||
|
_annots[i].VerBoundAnnot = verBoundAnnot;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region AnnotationJSObject
|
||||||
|
private Dictionary<string, IAnnotation> CreateAnnotationJSObject()
|
||||||
|
{
|
||||||
|
var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation);
|
||||||
|
|
||||||
|
if (Background is not null)
|
||||||
|
{
|
||||||
|
Background.Locate(_annots);
|
||||||
|
dict.Add(Background.Name.ToLower(), Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Lazy<Dictionary<string, IAnnotation>> _AnnotationJSObjectInitor;
|
||||||
|
|
||||||
|
public Dictionary<string, IAnnotation> AnnotationJSObject => _AnnotationJSObjectInitor.Value;
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
|
||||||
|
/// It is only the first annotation as a background for other annotations.
|
||||||
|
/// </summary>
|
||||||
|
public record Background : IAnnotation
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public double Margin { get; init; }
|
||||||
|
|
||||||
|
public string Name { get; } = "Background";
|
||||||
|
|
||||||
|
public double? Width { get; set; }
|
||||||
|
|
||||||
|
public double? Height { get; set; }
|
||||||
|
|
||||||
|
public double Left { get; set; }
|
||||||
|
|
||||||
|
public double Top { get; set; }
|
||||||
|
|
||||||
|
public Color? BackgroundColor { get; init; }
|
||||||
|
|
||||||
|
#region Border
|
||||||
|
public Color? BorderColor { get; init; }
|
||||||
|
|
||||||
|
public string? BorderStyle { get; init; }
|
||||||
|
|
||||||
|
public int? BorderWidth { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Locate(IEnumerable<IAnnotation> annotations)
|
||||||
|
{
|
||||||
|
// set Top
|
||||||
|
if (annotations.MinBy(a => a.Top)?.Top is double minTop)
|
||||||
|
Top = minTop;
|
||||||
|
|
||||||
|
// set Left
|
||||||
|
if (annotations.MinBy(a => a.Left)?.Left is double minLeft)
|
||||||
|
Left = minLeft;
|
||||||
|
|
||||||
|
// set Width
|
||||||
|
if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight)
|
||||||
|
Width = maxRight - Left;
|
||||||
|
|
||||||
|
// set Height
|
||||||
|
if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom)
|
||||||
|
Height = maxBottom - Top;
|
||||||
|
|
||||||
|
// add margins
|
||||||
|
Top -= Margin;
|
||||||
|
Left -= Margin;
|
||||||
|
Width += Margin * 2;
|
||||||
|
Height += Margin * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
EnvelopeGenerator.API/Models/PsPdfKitAnnotation/Color.cs
Normal file
10
EnvelopeGenerator.API/Models/PsPdfKitAnnotation/Color.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public record Color
|
||||||
|
{
|
||||||
|
public int R { get; init; } = 0;
|
||||||
|
|
||||||
|
public int G { get; init; } = 0;
|
||||||
|
|
||||||
|
public int B { get; init; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0;
|
||||||
|
|
||||||
|
public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public interface IAnnotation
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
double? Width { get; }
|
||||||
|
|
||||||
|
double? Height { get; }
|
||||||
|
|
||||||
|
double Left { get; }
|
||||||
|
|
||||||
|
double Top { get; }
|
||||||
|
|
||||||
|
Color? BackgroundColor { get; }
|
||||||
|
|
||||||
|
Color? BorderColor { get; }
|
||||||
|
|
||||||
|
string? BorderStyle { get; }
|
||||||
|
|
||||||
|
int? BorderWidth { get; }
|
||||||
|
}
|
||||||
17
EnvelopeGenerator.API/Models/TFARegParams.cs
Normal file
17
EnvelopeGenerator.API/Models/TFARegParams.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the parameters for two-factor authentication (2FA) registration.
|
||||||
|
/// </summary>
|
||||||
|
public class TFARegParams
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum allowed time for completing the registration process.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan TimeLimit { get; init; } = new(0, 30, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The deadline for registration, calculated as the current time plus the <see cref="TimeLimit"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks);
|
||||||
|
}
|
||||||
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal file
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for distributed caching.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CacheOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration section name in appsettings.json.
|
||||||
|
/// </summary>
|
||||||
|
public const string SectionName = "Cache";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signature cache expiration time.
|
||||||
|
/// If null, signatures will not expire automatically.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? SignatureCacheExpiration { get; set; }
|
||||||
|
}
|
||||||
355
EnvelopeGenerator.API/Program.cs
Normal file
355
EnvelopeGenerator.API/Program.cs
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
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.IdentityModel.Tokens;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
|
using EnvelopeGenerator.API.Middleware;
|
||||||
|
using EnvelopeGenerator.API.Options;
|
||||||
|
using NLog.Web;
|
||||||
|
using NLog;
|
||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
using EnvelopeGenerator.API;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
||||||
|
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()
|
||||||
|
.AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||||
|
});
|
||||||
|
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>();
|
||||||
|
});
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//Add EF 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(AuthScheme.Sender, 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// Per-envelope receiver scheme: reads the JWT from the cookie named
|
||||||
|
// AuthTokenSignFLOWReceiver.{envelope_key} where envelope_key is the
|
||||||
|
// last path segment of the request URL.
|
||||||
|
// This enables simultaneous authentication for multiple envelopes
|
||||||
|
// within the same browser session.
|
||||||
|
.AddJwtBearer(AuthScheme.Receiver, 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 =>
|
||||||
|
{
|
||||||
|
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
// Derive the envelope key from the last route segment: /{envelope_key}
|
||||||
|
var envelopeKey = paths?.LastOrDefault();
|
||||||
|
|
||||||
|
if (envelopeKey is not null)
|
||||||
|
{
|
||||||
|
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||||
|
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null)
|
||||||
|
context.Token = cookieToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
OnTokenValidated = context =>
|
||||||
|
{
|
||||||
|
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var envelopeKey = paths?.LastOrDefault();
|
||||||
|
|
||||||
|
var sub = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
||||||
|
?? context.Principal?.FindFirst("sub")?.Value;
|
||||||
|
|
||||||
|
if (envelopeKey is null || sub != envelopeKey)
|
||||||
|
context.Fail("Envelope key in the path does not match the token subject.");
|
||||||
|
|
||||||
|
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)
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
||||||
|
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||||
|
.RequireRole(Role.Sender)
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||||
|
.AddPolicy(AuthPolicy.Receiver, policy => policy
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Receiver)
|
||||||
|
.RequireAuthenticatedUser()
|
||||||
|
.RequireRole(Role.Receiver.Full, "receiver"))
|
||||||
|
.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();
|
||||||
|
|
||||||
|
// Cache options
|
||||||
|
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
|
||||||
|
|
||||||
|
// Distributed Cache - SQL Server
|
||||||
|
builder.Services.AddDistributedSqlServerCache(options =>
|
||||||
|
{
|
||||||
|
config.GetSection("Cache:SqlServer").Bind(options);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.ConnectionString))
|
||||||
|
{
|
||||||
|
options.ConnectionString = connStr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Envelope generator serives
|
||||||
|
#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>();
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
app.MapOpenApi();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 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.MapControllers();
|
||||||
|
|
||||||
|
// Catch-all YARP proxy — only forward requests that are not swagger/scalar/openapi paths.
|
||||||
|
app.MapReverseProxy();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Stopped program because of exception");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
|
-->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<WebPublishMethod>Package</WebPublishMethod>
|
||||||
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
|
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||||
|
<SiteUrlToLaunchAfterPublish />
|
||||||
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
|
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
||||||
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
51
EnvelopeGenerator.API/Properties/launchSettings.json
Normal file
51
EnvelopeGenerator.API/Properties/launchSettings.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:4176",
|
||||||
|
"sslPort": 44300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5131",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "sender",
|
||||||
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"angular": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "",
|
||||||
|
"applicationUrl": "https://localhost:7174;http://localhost:5131",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
EnvelopeGenerator.API/appsettings.Development.json
Normal file
29
EnvelopeGenerator.API/appsettings.Development.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReverseProxy": {
|
||||||
|
"Clusters": {
|
||||||
|
"receiver-ui": {
|
||||||
|
"Destinations": {
|
||||||
|
"primary": {
|
||||||
|
"Address": "https://localhost:52936"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AuthClientParams": {
|
||||||
|
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||||
|
"PublicKeys": [
|
||||||
|
{
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "sign-flow.digitaldata.works"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RetryDelay": "00:00:05"
|
||||||
|
}
|
||||||
|
}
|
||||||
256
EnvelopeGenerator.API/appsettings.json
Normal file
256
EnvelopeGenerator.API/appsettings.json
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
{
|
||||||
|
"UseSwagger": true,
|
||||||
|
"UseDbMigration": false,
|
||||||
|
"DiPMode": true,
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"AllowedOrigins": [ "http://localhost:4200", "http://172.24.12.39:9090", "https://localhost:8088", "http://localhost:5131", "http://localhost:7192" ],
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
|
||||||
|
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
"DirectorySearchOptions": {
|
||||||
|
"ServerName": "DD-VMP01-DC01",
|
||||||
|
"Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works",
|
||||||
|
"UserCacheExpirationDays": 1,
|
||||||
|
"CustomSearchFilters": {
|
||||||
|
"User": "(&(objectClass=user)(sAMAccountName=*))",
|
||||||
|
"Group": "(&(objectClass=group)(samAccountName=*))"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AuthClientParams": {
|
||||||
|
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||||
|
"PublicKeys": [
|
||||||
|
{
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "sign-flow.digitaldata.works"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RetryDelay": "00:00:05"
|
||||||
|
},
|
||||||
|
"AuthTokenKeys": {
|
||||||
|
"Cookie": "AuthToken",
|
||||||
|
"QueryString": "AuthToken",
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "sign-flow.digitaldata.works"
|
||||||
|
},
|
||||||
|
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
|
||||||
|
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self' 'nonce-{0}' 'unsafe-eval'",
|
||||||
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com:*",
|
||||||
|
"img-src 'self' data: https: blob:",
|
||||||
|
"font-src 'self' https://fonts.gstatic.com:*",
|
||||||
|
"connect-src 'self' https://nominatim.openstreetmap.org:* http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* blob:",
|
||||||
|
"frame-src 'self'",
|
||||||
|
"media-src 'self'",
|
||||||
|
"object-src 'self'"
|
||||||
|
],
|
||||||
|
"NLog": {
|
||||||
|
"throwConfigExceptions": true,
|
||||||
|
"variables": {
|
||||||
|
"logDirectory": "E:\\LogFiles\\Digital Data\\signFlow",
|
||||||
|
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Web"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"infoLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
},
|
||||||
|
"errorLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
},
|
||||||
|
"criticalLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Trace, Debug, Info, Warn, Error and *Fatal*
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"minLevel": "Info",
|
||||||
|
"maxLevel": "Warn",
|
||||||
|
"writeTo": "infoLogs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Error",
|
||||||
|
"writeTo": "errorLogs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Fatal",
|
||||||
|
"writeTo": "criticalLogs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ContactLink": {
|
||||||
|
"Label": "Kontakt",
|
||||||
|
"Href": "https://digitaldata.works/",
|
||||||
|
"HrefLang": "de",
|
||||||
|
"Target": "_blank",
|
||||||
|
"Title": "Digital Data GmbH"
|
||||||
|
},
|
||||||
|
/* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx).
|
||||||
|
To add a new language, first you should write the required resx file.
|
||||||
|
first is the default culture name. */
|
||||||
|
"Cultures": [
|
||||||
|
{
|
||||||
|
"Language": "de-DE",
|
||||||
|
"FIClass": "fi-de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Language": "en-US",
|
||||||
|
"FIClass": "fi-us"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"DisableMultiLanguage": false,
|
||||||
|
"Regexes": [
|
||||||
|
{
|
||||||
|
"Pattern": "/^\\p{L}+(?:([\\ \\-\\']|(\\.\\ ))\\p{L}+)*$/u",
|
||||||
|
"Name": "City",
|
||||||
|
"Platforms": [ ".NET" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Pattern": "/^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$/",
|
||||||
|
"Name": "City",
|
||||||
|
"Platforms": [ "javascript" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"CustomImages": {
|
||||||
|
"App": {
|
||||||
|
"Src": "/img/DD_signFLOW_LOGO.png",
|
||||||
|
"Classes": {
|
||||||
|
"Main": "signFlow-logo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Company": {
|
||||||
|
"Src": "/img/digital_data.svg",
|
||||||
|
"Classes": {
|
||||||
|
"Show": "dd-show-logo",
|
||||||
|
"Locked": "dd-locked-logo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DispatcherParams": {
|
||||||
|
"SendingProfile": 1,
|
||||||
|
"AddedWho": "DDEnvelopGenerator",
|
||||||
|
"ReminderTypeId": 202377,
|
||||||
|
"EmailAttmt1": ""
|
||||||
|
},
|
||||||
|
"MailParams": {
|
||||||
|
"Placeholders": {
|
||||||
|
"[NAME_PORTAL]": "signFlow",
|
||||||
|
"[SIGNATURE_TYPE]": "signieren",
|
||||||
|
"[REASON]": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GtxMessagingParams": {
|
||||||
|
"Uri": "https://rest.gtx-messaging.net",
|
||||||
|
"Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json",
|
||||||
|
"Headers": {},
|
||||||
|
"QueryParams": {
|
||||||
|
"from": "signFlow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TFARegParams": {
|
||||||
|
"TimeLimit": "00:30:00"
|
||||||
|
},
|
||||||
|
"DbTriggerParams": {
|
||||||
|
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
|
"EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
|
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
||||||
|
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
||||||
|
"Receiver": [],
|
||||||
|
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
||||||
|
},
|
||||||
|
"Cache": {
|
||||||
|
"SignatureCacheExpiration": null,
|
||||||
|
"SqlServer": {
|
||||||
|
"ConnectionString": null,
|
||||||
|
"SchemaName": "dbo",
|
||||||
|
"TableName": "TBDD_CACHE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MainPageTitle": null,
|
||||||
|
"AnnotationParams": {
|
||||||
|
"Background": {
|
||||||
|
"Margin": 0.20,
|
||||||
|
"BackgroundColor": {
|
||||||
|
"R": 222,
|
||||||
|
"G": 220,
|
||||||
|
"B": 215
|
||||||
|
},
|
||||||
|
"BorderColor": {
|
||||||
|
"R": 204,
|
||||||
|
"G": 202,
|
||||||
|
"B": 198
|
||||||
|
},
|
||||||
|
"BorderStyle": "underline",
|
||||||
|
"BorderWidth": 4
|
||||||
|
},
|
||||||
|
"DefaultAnnotation": {
|
||||||
|
"Width": 1,
|
||||||
|
"Height": 0.5,
|
||||||
|
"MarginTop": 1
|
||||||
|
},
|
||||||
|
"Annotations": [
|
||||||
|
{
|
||||||
|
"Name": "Signature",
|
||||||
|
"MarginTop": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "PositionLabel",
|
||||||
|
"VerBoundAnnotName": "Signature",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": 0.22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Position",
|
||||||
|
"VerBoundAnnotName": "PositionLabel",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": -0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "CityLabel",
|
||||||
|
"VerBoundAnnotName": "Position",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "City",
|
||||||
|
"VerBoundAnnotName": "CityLabel",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": -0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "DateLabel",
|
||||||
|
"VerBoundAnnotName": "City",
|
||||||
|
"WidthRatio": 1.55,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Date",
|
||||||
|
"VerBoundAnnotName": "DateLabel",
|
||||||
|
"WidthRatio": 1.55,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": -0.1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
185
EnvelopeGenerator.API/yarp.json
Normal file
185
EnvelopeGenerator.API/yarp.json
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"ReverseProxy": {
|
||||||
|
"Routes": {
|
||||||
|
"receiver-ui-root": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 300,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-sender": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 100,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/sender/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-envelope": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 100,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/envelope/{EnvelopeKey}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-envelope-dxreportviewer": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 90,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/envelope/{EnvelopeKey}/DxReportViewer",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-blazor-framework": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/_framework/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-blazor-content": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/_content/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-static-css": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/css/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{
|
||||||
|
"ResponseHeader": "Cache-Control",
|
||||||
|
"Set": "no-cache, no-store, must-revalidate",
|
||||||
|
"When": "Always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-static-js": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/js/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{
|
||||||
|
"ResponseHeader": "Cache-Control",
|
||||||
|
"Set": "no-cache, no-store, must-revalidate",
|
||||||
|
"When": "Always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-fake-data": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/fake-data/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-appsettings": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/appsettings.json",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-appsettings-dev": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/appsettings.Development.json",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-styles": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/EnvelopeGenerator.ReceiverUI.styles.css",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-fonts": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/fonts/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-images": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/images/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth-login": {
|
||||||
|
"ClusterId": "auth-hub",
|
||||||
|
"Match": {
|
||||||
|
"Path": "/api/auth",
|
||||||
|
"Methods": [ "POST" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/api/auth/sign-flow" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"auth-envelope-receiver-login": {
|
||||||
|
"ClusterId": "auth-hub",
|
||||||
|
"Match": {
|
||||||
|
"Path": "/api/Auth/envelope-receiver/{key}",
|
||||||
|
"Methods": [ "POST" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
|
||||||
|
{
|
||||||
|
"QueryValueParameter": "cookie",
|
||||||
|
"Set": "true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Clusters": {
|
||||||
|
"receiver-ui": {
|
||||||
|
"Destinations": {
|
||||||
|
"primary": {
|
||||||
|
"Address": "https://localhost:52936"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth-hub": {
|
||||||
|
"Destinations": {
|
||||||
|
"primary": {
|
||||||
|
"Address": "http://172.24.12.39:9090"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using FluentValidation;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Behaviors;
|
|
||||||
|
|
||||||
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
|
||||||
where TRequest : notnull
|
|
||||||
{
|
|
||||||
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
|
||||||
|
|
||||||
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
|
||||||
{
|
|
||||||
_validators = validators;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TResponse> Handle(
|
|
||||||
TRequest request,
|
|
||||||
RequestHandlerDelegate<TResponse> next,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_validators.Any())
|
|
||||||
{
|
|
||||||
var context = new ValidationContext<TRequest>(request);
|
|
||||||
var failures = _validators
|
|
||||||
.Select(v => v.Validate(context))
|
|
||||||
.SelectMany(r => r.Errors)
|
|
||||||
.Where(f => f is not null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (failures.Count > 0)
|
|
||||||
{
|
|
||||||
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
|
|
||||||
throw new BadRequestException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ESignParams
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string ApplicationName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string DefaultReason { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class SignatureFieldParams
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public double Width { get; init; } = 1.77;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public double Height { get; init; } = 1.96;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
// TODO: Add the “Reason” property and integrate with UI and the digital signature process.
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
public class DocReceiverElementDto : IDocReceiverElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the element.
|
/// Gets or sets the unique identifier of the element.
|
||||||
@@ -20,21 +18,11 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int DocumentId { get; set; }
|
public int DocumentId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public virtual DocumentDto? Document { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the identifier of the receiver.
|
/// Gets or sets the identifier of the receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ReceiverId { get; set; }
|
public int ReceiverId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public virtual ReceiverDto? Receiver { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of the element.
|
/// Gets or sets the type of the element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -134,41 +122,4 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? Ink { get; set; }
|
public byte[]? Ink { get; set; }
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Returns the stored signature bytes as an image payload with detected MIME type.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException">Thrown when signature image bytes do not exist.</exception>
|
|
||||||
public (byte[] Bytes, string ContentType, string FileExtension) ToImage()
|
|
||||||
{
|
|
||||||
if (Ink is not { Length: > 0 })
|
|
||||||
throw new InvalidOperationException("Signature image (Ink) does not exist.");
|
|
||||||
|
|
||||||
if (HasPrefix(Ink, 0x89, 0x50, 0x4E, 0x47))
|
|
||||||
return (Ink, "image/png", ".png");
|
|
||||||
|
|
||||||
if (HasPrefix(Ink, 0xFF, 0xD8, 0xFF))
|
|
||||||
return (Ink, "image/jpeg", ".jpg");
|
|
||||||
|
|
||||||
if (HasPrefix(Ink, 0x52, 0x49, 0x46, 0x46) &&
|
|
||||||
Ink.Length >= 12 &&
|
|
||||||
Ink[8] == 0x57 && Ink[9] == 0x45 && Ink[10] == 0x42 && Ink[11] == 0x50)
|
|
||||||
return (Ink, "image/webp", ".webp");
|
|
||||||
|
|
||||||
return (Ink, "application/octet-stream", ".bin");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HasPrefix(byte[] data, params byte[] prefix)
|
|
||||||
{
|
|
||||||
if (data.Length < prefix.Length)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (var i = 0; i < prefix.Length; i++)
|
|
||||||
{
|
|
||||||
if (data[i] != prefix[i])
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Data Transfer Object representing document metadata and elements without binary payload.
|
|
||||||
/// </summary>
|
|
||||||
public record DocumentDetailsDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the unique identifier of the document.
|
|
||||||
/// </summary>
|
|
||||||
public int Id { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the envelope ID to which the document belongs.
|
|
||||||
/// </summary>
|
|
||||||
public int EnvelopeId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the date and time when the document was added.
|
|
||||||
/// </summary>
|
|
||||||
public DateTime AddedWhen { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
|
|
||||||
}
|
|
||||||
@@ -3,25 +3,30 @@
|
|||||||
/// <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>
|
||||||
public record DocumentDto : DocumentDetailsDto
|
public class DocumentDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets or sets the unique identifier of the document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DocumentDto() { }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets or sets the envelope ID to which the document belongs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="byteData"></param>
|
public int EnvelopeId { get; set; }
|
||||||
/// <param name="details"></param>
|
|
||||||
public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
|
/// <summary>
|
||||||
{
|
/// Gets or sets the date and time when the document was added.
|
||||||
ByteData = byteData;
|
/// </summary>
|
||||||
}
|
public DateTime AddedWhen { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the binary data of the document, if available.
|
/// Gets or sets the binary data of the document, if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? ByteData { get; set; }
|
public byte[]? ByteData { get; set; }
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ContractType? ContractType { get; set; }
|
public int? ContractType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default value is 'de-DE'
|
/// Default value is 'de-DE'
|
||||||
@@ -84,42 +84,7 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CertificationType? CertificationType { get; set; }
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int? FinalEmailToCreator { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int? FinalEmailToReceivers { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public bool? SendReminderEmails { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int? FirstReminderDays { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int? ReminderIntervalDays { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int? ExpiresWhenDays { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int? ExpiresWarningWhenDays { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -170,4 +135,4 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<HistoryDto>? Histories { get; set; }
|
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||||
}
|
}
|
||||||
@@ -60,11 +60,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>
|
||||||
@@ -88,4 +83,4 @@ public record EnvelopeReceiverDto
|
|||||||
h.Receiver?.Id == ReceiverId &&
|
h.Receiver?.Id == ReceiverId &&
|
||||||
h.Status == EnvelopeStatus.DocumentSigned
|
h.Status == EnvelopeStatus.DocumentSigned
|
||||||
) ?? false;
|
) ?? false;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
@@ -30,7 +28,7 @@ public class EnvelopeTypeDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the certification type identifier.
|
/// Gets or sets the certification type identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CertificationType? CertificationType { get; set; }
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether an access code is required.
|
/// Gets or sets a value indicating whether an access code is required.
|
||||||
@@ -80,5 +78,5 @@ public class EnvelopeTypeDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the contract type associated with the envelope type.
|
/// Gets or sets the contract type associated with the envelope type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ContractType? ContractType { get; set; }
|
public int? ContractType { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
public sealed class PdfPageImageDto
|
|
||||||
{
|
|
||||||
public int PageNumber { get; init; }
|
|
||||||
|
|
||||||
public double WidthPt { get; init; }
|
|
||||||
|
|
||||||
public double HeightPt { get; init; }
|
|
||||||
|
|
||||||
public int WidthPx { get; init; }
|
|
||||||
|
|
||||||
public int HeightPx { get; init; }
|
|
||||||
|
|
||||||
public string ImageDataUrl { get; init; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
public sealed class PdfRenderRequestDto
|
|
||||||
{
|
|
||||||
public byte[]? PdfBytes { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Enums;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public sealed record PdfSignOptionsDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public long DocumentElementId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string Reason { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string Location { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string? Email { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string? Phone { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string ApplicationName { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public PdfSignatureCertificationLevel CertificationLevel { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public PdfSignatureAppearanceDto? Appearance { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public enum PdfSignatureCoordinateUnit
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
Point = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
Inch = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public enum PdfSignatureCoordinateOrigin
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
BottomLeft = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
TopLeft = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="PageNumber"></param>
|
|
||||||
/// <param name="Left"></param>
|
|
||||||
/// <param name="Bottom"></param>
|
|
||||||
/// <param name="Width"></param>
|
|
||||||
/// <param name="Height"></param>
|
|
||||||
/// <param name="ShowLabels"></param>
|
|
||||||
/// <param name="ShowName"></param>
|
|
||||||
/// <param name="ShowDate"></param>
|
|
||||||
/// <param name="ShowReason"></param>
|
|
||||||
/// <param name="ShowLocation"></param>
|
|
||||||
/// <param name="ShowDistinguishedName"></param>
|
|
||||||
/// <param name="DateTimeFormat"></param>
|
|
||||||
/// <param name="CoordinateUnit"></param>
|
|
||||||
/// <param name="CoordinateOrigin"></param>
|
|
||||||
public sealed record PdfSignatureAppearanceDto(
|
|
||||||
int PageNumber,
|
|
||||||
double Left,
|
|
||||||
double Bottom,
|
|
||||||
double Width,
|
|
||||||
double Height,
|
|
||||||
bool ShowLabels = false,
|
|
||||||
bool ShowName = false,
|
|
||||||
bool ShowDate = false,
|
|
||||||
bool ShowReason = false,
|
|
||||||
bool ShowLocation = false,
|
|
||||||
bool ShowDistinguishedName = false,
|
|
||||||
string DateTimeFormat = "dd.MM.yyyy HH:mm:ss zzz",
|
|
||||||
PdfSignatureCoordinateUnit CoordinateUnit = PdfSignatureCoordinateUnit.Point,
|
|
||||||
PdfSignatureCoordinateOrigin CoordinateOrigin = PdfSignatureCoordinateOrigin.BottomLeft)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="FamilyName"></param>
|
|
||||||
/// <param name="Size"></param>
|
|
||||||
/// <param name="IsBold"></param>
|
|
||||||
/// <param name="IsItalic"></param>
|
|
||||||
/// <param name="IsUnderline"></param>
|
|
||||||
public sealed record FontDto(string FamilyName = "Helvetica", float Size = 10, bool IsBold = false, bool IsItalic = false, bool IsUnderline = false)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public RgbaColorDto Color { get; init; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="R"></param>
|
|
||||||
/// <param name="G"></param>
|
|
||||||
/// <param name="B"></param>
|
|
||||||
public sealed record RgbaColorDto(double R = 0, double G = 0, double B = 0);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public FontDto Font { get; init; } = new();
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
public sealed record PdfSignaturePrincipalDto(
|
|
||||||
string? CommonName,
|
|
||||||
string? Organization,
|
|
||||||
string? Country,
|
|
||||||
string? Full);
|
|
||||||
|
|
||||||
public sealed record PdfSignatureCertificateInfoDto(
|
|
||||||
PdfSignaturePrincipalDto IssuedTo,
|
|
||||||
PdfSignaturePrincipalDto IssuedBy,
|
|
||||||
string? SerialNumber,
|
|
||||||
DateTimeOffset? ValidFrom,
|
|
||||||
DateTimeOffset? ValidTo,
|
|
||||||
bool SelfSigned,
|
|
||||||
bool Expired,
|
|
||||||
bool NotYetValid,
|
|
||||||
bool IsSigner,
|
|
||||||
int? Version,
|
|
||||||
string? KeyAlgorithm,
|
|
||||||
string? SignatureAlgorithm,
|
|
||||||
string? Pem,
|
|
||||||
int? KeySizeBits);
|
|
||||||
|
|
||||||
public sealed record PdfSignatureCheckItemDto(
|
|
||||||
bool CoversWholeDoc,
|
|
||||||
string? Reason,
|
|
||||||
string? BusinessReason,
|
|
||||||
string? Location,
|
|
||||||
string? DeclaredName,
|
|
||||||
string? ContactInfo,
|
|
||||||
string? Email,
|
|
||||||
string? Phone,
|
|
||||||
string? DigestAlgorithm,
|
|
||||||
string? SignatureAlgorithm,
|
|
||||||
string? SignerName,
|
|
||||||
string? Issuer,
|
|
||||||
string? SerialNumber,
|
|
||||||
DateTimeOffset? ValidFrom,
|
|
||||||
DateTimeOffset? ValidTo,
|
|
||||||
string ExpiryCheckedAgainst,
|
|
||||||
IReadOnlyList<PdfSignatureCertificateInfoDto> Certificates,
|
|
||||||
bool Unchanged,
|
|
||||||
string? FieldName,
|
|
||||||
bool IsCryptographicallyValid,
|
|
||||||
string? SigningTime,
|
|
||||||
string? Filter,
|
|
||||||
string? SubFilter,
|
|
||||||
string? ApplicationName,
|
|
||||||
long? DocumentElementId,
|
|
||||||
string? OriginalDocumentSha256,
|
|
||||||
string? MessageDigestHex,
|
|
||||||
string? Error);
|
|
||||||
|
|
||||||
public sealed record PdfSignatureCheckResultDto(
|
|
||||||
bool Signed,
|
|
||||||
IReadOnlyList<PdfSignatureCheckItemDto> Signatures);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
|
|
||||||
public class RecentReceiverSuggestionDto
|
|
||||||
{
|
|
||||||
public required string FullName { get; init; }
|
|
||||||
|
|
||||||
public required string EmailAddress { get; init; }
|
|
||||||
|
|
||||||
public required DateTime LastUsedAt { get; init; }
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@ public sealed record Signature
|
|||||||
public string? Position
|
public string? Position
|
||||||
{
|
{
|
||||||
get => _position;
|
get => _position;
|
||||||
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,4 +62,4 @@ public sealed record Signature
|
|||||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Place { get; init; }
|
public required string Place { get; init; }
|
||||||
}
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
|
|
||||||
/// </summary>
|
|
||||||
public sealed record UserPreferencesDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The active DevExpress theme name.
|
|
||||||
/// Allowed values: "blazing-berry", "fluent", "purple"
|
|
||||||
/// </summary>
|
|
||||||
public string ThemeName { get; set; } = "blazing-berry";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether dark mode is active.
|
|
||||||
/// Default: false (light mode).
|
|
||||||
/// </summary>
|
|
||||||
public bool IsDarkMode { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
|
|
||||||
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
|
|
||||||
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
|
|
||||||
/// </summary>
|
|
||||||
public Dictionary<string, string> GridLayouts { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The authenticated user's display name ("{GivenName} {Surname}").
|
|
||||||
/// Populated by the server on GET responses from JWT claims.
|
|
||||||
/// NOT persisted in cache — injected at read time by UserPreferencesController.
|
|
||||||
/// Ignored on PUT requests.
|
|
||||||
/// </summary>
|
|
||||||
public string? DisplayName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static UserPreferencesDto Default { get; } = new UserPreferencesDto();
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Enums;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public enum PdfSignatureCertificationLevel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// All changes are allowed
|
|
||||||
/// </summary>
|
|
||||||
NoCertification = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// No changes are allowed
|
|
||||||
/// </summary>
|
|
||||||
NoChangesAllowed,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows users to fill in forms and sign the document.
|
|
||||||
/// </summary>
|
|
||||||
FillForms,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows users to fill in forms, create, modify and delete annotations, and sign the document.
|
|
||||||
/// </summary>
|
|
||||||
FillFormsAndAnnotate
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public interface IPdfPageImageRenderer
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pdfBytes"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Stream ComposeSignaturePreviewPng(ISignatureInkPayload element);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bytes"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool IsSupportedImageFormat(byte[] bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class PdfPageImageRendererExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="renderer"></param>
|
|
||||||
/// <param name="inkBytes"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="BadRequestException"></exception>
|
|
||||||
public static byte[] EnsureSupportedImageBytes(this IPdfPageImageRenderer renderer, byte[] inkBytes)
|
|
||||||
{
|
|
||||||
if (!renderer.IsSupportedImageFormat(inkBytes))
|
|
||||||
throw new BadRequestException("Only PNG and JPEG signature formats are supported.");
|
|
||||||
|
|
||||||
return inkBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] ComposeSignaturePreviewPngBytes(this IPdfPageImageRenderer renderer, ISignatureInkPayload element)
|
|
||||||
{
|
|
||||||
using var stream = renderer.ComposeSignaturePreviewPng(element);
|
|
||||||
|
|
||||||
if (stream is MemoryStream memoryStream)
|
|
||||||
return memoryStream.ToArray();
|
|
||||||
|
|
||||||
using var copy = new MemoryStream();
|
|
||||||
stream.CopyTo(copy);
|
|
||||||
return copy.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public interface IRequestContext
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
string? Origin { get; }
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines PDF digital-signing operations for the application boundary.
|
|
||||||
/// </summary>
|
|
||||||
public interface IPdfDigitalSignatureService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a digital signature for the provided PDF bytes and returns
|
|
||||||
/// a new signed PDF document.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inputPdf">Source PDF content stream.</param>
|
|
||||||
/// <param name="outputPdf">Destination stream for signed PDF output.</param>
|
|
||||||
/// <param name="options">Optional signing options that map to PDF signature metadata fields.</param>
|
|
||||||
/// <param name="image">Optional image stream for signature appearance.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
|
||||||
Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether the provided PDF contains signature field(s) and whether
|
|
||||||
/// those signatures are cryptographically valid.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inputPdf">PDF content stream to inspect.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
|
||||||
/// <returns>A structured result containing signature presence and per-signature details.</returns>
|
|
||||||
Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
|
|
||||||
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
|
|
||||||
/// Cache key schema: "signflow.user-preferences:{username}"
|
|
||||||
/// </summary>
|
|
||||||
public interface IUserPreferencesService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves preferences for the given user.
|
|
||||||
/// Always returns a valid object — never throws, never returns null.
|
|
||||||
/// Returns defaults if no entry exists in cache.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id"></param>
|
|
||||||
/// <param name="ct">Cancellation token.</param>
|
|
||||||
Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persists the given preferences for the user in the distributed cache.
|
|
||||||
/// Failures are non-fatal and silently ignored.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id"></param>
|
|
||||||
/// <param name="preferences">The preferences to persist.</param>
|
|
||||||
/// <param name="ct">Cancellation token.</param>
|
|
||||||
Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record DocumentReceivedNotification : EnvelopeNotificationBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string DocumentTitle { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string NameReceiver { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string NameSender { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// [SIGNATURE_TYPE]
|
|
||||||
/// </summary>
|
|
||||||
public required bool IsReadAndConfirm { private get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string SignatureType => IsReadAndConfirm ? "Signieren" : "Lesen und bestätigen";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required bool AdvancedSignature { private get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string DocumentProcess => AdvancedSignature ? " und elektronisch unterschreiben" : string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required Uri Origin { private get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string EnvelopeUuid { private get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string ReceiverSignature { private get; init; }
|
|
||||||
|
|
||||||
private Uri EnvelopeURL => new(Origin, $"Envelope/{(EnvelopeUuid, ReceiverSignature).ToEnvelopeKey()}");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string LinkToDocument => EnvelopeURL.AbsoluteUri;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string LinkToDocumentText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var urlText = EnvelopeURL.ToString();
|
|
||||||
return $"{urlText[..Math.Min(urlText.Length, 40)]}..";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string Message { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string NamePortal { get; set; } = "signFLOW Portal";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public override EmailTemplateType GetTemplateType()
|
|
||||||
{
|
|
||||||
return EmailTemplateType.DocumentReceived;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record DocumentSignedNotification : EnvelopeNotificationBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string DocumentTitle { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string NameReceiver { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string NamePortal { get; init; } = "signFLOW Portal";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required bool IsReadAndConfirm { private get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string ReceiverAction => IsReadAndConfirm ? "bestätigt" : "unterschrieben";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string FinalStatus => IsReadAndConfirm ? "Lesebestätigung" : "Signatur";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string FinalAction => IsReadAndConfirm ? "Empfänger bestätigt" : "Vertragspartner unterzeichnet";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public override EmailTemplateType GetTemplateType() => EmailTemplateType.DocumentSigned;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Umschlag abgeschlossen: '[DOCUMENT_TITLE]'
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Guten Tag [NAME_RECEIVER],<br /> <br /> Der Vorgang <B><I>'[DOCUMENT_TITLE]'</I></B> wurde erfolgreich abgeschlossen.<br /> <br /> Sie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public abstract record EnvelopeNotificationBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public abstract EmailTemplateType GetTemplateType();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<(string FileName, byte[] Content)>? Attachments { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public interface IEnvelopeNotificationService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TEnvelopeNotificationBase"></typeparam>
|
|
||||||
/// <param name="receiverEmail"></param>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
|
|
||||||
where TEnvelopeNotificationBase : EnvelopeNotificationBase;
|
|
||||||
}
|
|
||||||
@@ -28,11 +28,8 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>()
|
CreateMap<Envelope, EnvelopeDto>();
|
||||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
|
||||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<Document, DocumentDetailsDto>();
|
|
||||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
@@ -69,4 +66,4 @@ public class MappingProfile : Profile
|
|||||||
? new SmsResponse() { Ok = true }
|
? new SmsResponse() { Ok = true }
|
||||||
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
|
||||||
|
|
||||||
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
|
||||||
{
|
|
||||||
public required Envelope Envelope { get; init; }
|
|
||||||
|
|
||||||
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
|
||||||
|
|
||||||
public required string Reason { get; init; }
|
|
||||||
|
|
||||||
public required string SenderReference { get; init; }
|
|
||||||
|
|
||||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
|
||||||
|
|
||||||
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
|
||||||
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
|
||||||
|
|
||||||
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
|
||||||
{
|
|
||||||
public SendDeletedMailHandler(
|
|
||||||
IRepository<EmailTemplate> tempRepo,
|
|
||||||
IRepository<EmailOut> emailOutRepo,
|
|
||||||
IOptions<MailParams> mailParamsOptions,
|
|
||||||
IOptions<DispatcherParams> dispatcherParamsOptions)
|
|
||||||
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
|
||||||
{
|
|
||||||
emailOut.ReferenceString = notification.EmailAddress;
|
|
||||||
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
|
||||||
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
|
||||||
{
|
|
||||||
var senderName = notification.Envelope.User?.Email;
|
|
||||||
if (string.IsNullOrWhiteSpace(senderName))
|
|
||||||
senderName = notification.SenderReference;
|
|
||||||
|
|
||||||
return new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
|
||||||
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
|
||||||
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
|
||||||
["[REASON]"] = notification.Reason
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -97,10 +96,6 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
|||||||
/// <exception cref="NotImplementedException"></exception>
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
|
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
// do not send email if the envelope is deleted and the status is less than or equal to EnvelopeSaved
|
|
||||||
if (notification is EnvelopeDeletedNotification delNot && delNot.Envelope.Status <= EnvelopeStatus.EnvelopeSaved)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var placeHolders = CreatePlaceHolders(notification);
|
var placeHolders = CreatePlaceHolders(notification);
|
||||||
|
|
||||||
var temp = await TempRepo
|
var temp = await TempRepo
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ using System.Reflection;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
using EnvelopeGenerator.Application.Common.Behaviors;
|
|
||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application;
|
namespace EnvelopeGenerator.Application;
|
||||||
|
|
||||||
@@ -49,10 +47,8 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
|
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
|
||||||
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
||||||
services.Configure<SignatureFieldParams>(config.GetSection(nameof(SignatureFieldParams)));
|
|
||||||
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
||||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||||
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
|
|
||||||
|
|
||||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||||
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||||
@@ -63,32 +59,24 @@ public static class DependencyInjection
|
|||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
// Generic request validation
|
|
||||||
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
|
||||||
|
|
||||||
// Register SignCommand pipeline behaviors in execution order
|
// Register SignCommand pipeline behaviors in execution order
|
||||||
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
|
|
||||||
// 1. AnnotationBehavior - Saves annotations (executes second)
|
// 1. AnnotationBehavior - Saves annotations (executes second)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
|
|
||||||
|
|
||||||
// 2. AnnotationBehavior - Saves annotations for legacy web flow
|
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||||
|
|
||||||
// 3. DocStatusBehavior - Creates document status
|
// 2. DocStatusBehavior - Creates document status (executes third)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||||
|
|
||||||
// 4. HistoryBehavior - Records history
|
// 3. HistoryBehavior - Records history (executes fourth)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||||
|
|
||||||
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
|
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,11 +43,7 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
|
|||||||
// If EnvelopeReceiver is not provided, query it from database
|
// If EnvelopeReceiver is not provided, query it from database
|
||||||
if (request.EnvelopeReceiver is null)
|
if (request.EnvelopeReceiver is null)
|
||||||
{
|
{
|
||||||
var er = await _erRepo.Query
|
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
||||||
.Where(request, notnull: true)
|
|
||||||
.Include(x => x.Envelope)
|
|
||||||
.Include(x => x.Receiver)
|
|
||||||
.SingleOrDefaultAsync(cancellationToken)
|
|
||||||
?? throw new NotFoundException("EnvelopeReceiver not found");
|
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||||
|
|
||||||
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|||||||
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||||
|
|
||||||
var elements = await _elementRepo
|
var elements = await _elementRepo
|
||||||
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
||||||
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
.Where(e => e.ReceiverId == request.Receiver.Id)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
foreach (var element in elements)
|
foreach (var element in elements)
|
||||||
|
|||||||
@@ -14,15 +14,6 @@ namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int? Id { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public bool IncludeDocument { get; set; } = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,10 +47,7 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
|
|||||||
{
|
{
|
||||||
var q = _repository.Query;
|
var q = _repository.Query;
|
||||||
|
|
||||||
if (request.Id is not null)
|
if(request.Envelope.Id is int envelopeId)
|
||||||
q = q.Where(e => e.Id == request.Id);
|
|
||||||
|
|
||||||
if (request.Envelope.Id is int envelopeId)
|
|
||||||
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
|
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
|
||||||
|
|
||||||
if (request.Envelope.Uuid is string envelopeUuid)
|
if (request.Envelope.Uuid is string envelopeUuid)
|
||||||
@@ -71,9 +59,6 @@ public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceive
|
|||||||
if (request.Receiver.Signature is string signature)
|
if (request.Receiver.Signature is string signature)
|
||||||
q = q.Where(e => e.Receiver.Signature == signature);
|
q = q.Where(e => e.Receiver.Signature == signature);
|
||||||
|
|
||||||
if( request.IncludeDocument)
|
|
||||||
q = q.Include(e => e.Document);
|
|
||||||
|
|
||||||
var elements = await q.ToListAsync(cancellationToken);
|
var elements = await q.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Documents.Queries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a query to read document metadata/details without binary payload.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
|
|
||||||
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
|
||||||
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles queries for reading document details without loading BYTE_DATA.
|
|
||||||
/// </summary>
|
|
||||||
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
|
||||||
{
|
|
||||||
private readonly IRepository<Document> _repo;
|
|
||||||
|
|
||||||
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo)
|
|
||||||
{
|
|
||||||
_repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var docQuery = _repo.Query.AsNoTracking();
|
|
||||||
|
|
||||||
if (query.UserId is int userId)
|
|
||||||
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
|
||||||
|
|
||||||
// shortcut: manual select avoids AutoMapper ProjectTo pulling BYTE_DATA and the full navigation
|
|
||||||
// graph (Envelope → History → User etc.) which caused an extremely slow query (~87s).
|
|
||||||
// Annotations navigation is IEnumerable<T> on the entity (not ICollection<T>), so EF Core
|
|
||||||
// cannot translate it in a Select projection — we skip it entirely here; sender editor only
|
|
||||||
// needs element coordinates, not annotation payloads.
|
|
||||||
var raw = await docQuery
|
|
||||||
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
|
||||||
.Select(d => new
|
|
||||||
{
|
|
||||||
d.Id,
|
|
||||||
d.EnvelopeId,
|
|
||||||
d.AddedWhen,
|
|
||||||
Elements = d.Elements == null ? null : d.Elements.Select(e => new
|
|
||||||
{
|
|
||||||
e.Id,
|
|
||||||
e.DocumentId,
|
|
||||||
e.ReceiverId,
|
|
||||||
e.ElementType,
|
|
||||||
e.X,
|
|
||||||
e.Y,
|
|
||||||
e.Width,
|
|
||||||
e.Height,
|
|
||||||
e.Page,
|
|
||||||
e.Required,
|
|
||||||
e.Tooltip,
|
|
||||||
e.ReadOnly,
|
|
||||||
e.AnnotationIndex,
|
|
||||||
e.AddedWhen,
|
|
||||||
e.ChangedWhen,
|
|
||||||
e.FullName,
|
|
||||||
e.Position,
|
|
||||||
e.Place,
|
|
||||||
}).ToList()
|
|
||||||
})
|
|
||||||
.FirstOrDefaultAsync(cancel);
|
|
||||||
|
|
||||||
if (raw is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new DocumentDetailsDto
|
|
||||||
{
|
|
||||||
Id = raw.Id,
|
|
||||||
EnvelopeId = raw.EnvelopeId,
|
|
||||||
AddedWhen = raw.AddedWhen,
|
|
||||||
Elements = raw.Elements?.Select(e => new DocReceiverElementDto
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
DocumentId = e.DocumentId,
|
|
||||||
ReceiverId = e.ReceiverId,
|
|
||||||
ElementType = e.ElementType,
|
|
||||||
X = e.X,
|
|
||||||
Y = e.Y,
|
|
||||||
Width = e.Width,
|
|
||||||
Height = e.Height,
|
|
||||||
Page = e.Page,
|
|
||||||
Required = e.Required,
|
|
||||||
Tooltip = e.Tooltip,
|
|
||||||
ReadOnly = e.ReadOnly,
|
|
||||||
AnnotationIndex = e.AnnotationIndex,
|
|
||||||
AddedWhen = e.AddedWhen,
|
|
||||||
ChangedWhen = e.ChangedWhen,
|
|
||||||
FullName = e.FullName,
|
|
||||||
Position = e.Position,
|
|
||||||
Place = e.Place,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Documents.Queries;
|
|
||||||
|
|
||||||
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
|
|
||||||
{
|
|
||||||
public ReadDocumentDetailsQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.EnvelopeId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("EnvelopeId is required.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,10 +53,7 @@ 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();
|
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations);
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Documents.Queries;
|
|
||||||
|
|
||||||
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
|
|
||||||
{
|
|
||||||
public ReadDocumentQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x)
|
|
||||||
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
|
|
||||||
.WithMessage("Id or EnvelopeId is required.");
|
|
||||||
|
|
||||||
RuleFor(x => x.Id)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.When(x => x.Id.HasValue)
|
|
||||||
.WithMessage("Id must be greater than 0.");
|
|
||||||
|
|
||||||
RuleFor(x => x.EnvelopeId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.When(x => x.EnvelopeId.HasValue)
|
|
||||||
.WithMessage("EnvelopeId must be greater than 0.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -20,8 +19,6 @@
|
|||||||
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
||||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Befehl zur Erstellung eines Umschlags.
|
/// Befehl zur Erstellung eines Umschlags.
|
||||||
@@ -4,7 +4,7 @@ using MediatR;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the creation of an envelope along with its associated document and recipients.
|
/// Handles the creation of an envelope along with its associated document and recipients.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using EnvelopeGenerator.Domain.Exceptions;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record RemoveEnvelopeReceiverCommand : IRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required int EnvelopeId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required int ReceiverId { get; init; }
|
|
||||||
|
|
||||||
internal int? UserId { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public RemoveEnvelopeReceiverCommand Authenticated(int userId)
|
|
||||||
{
|
|
||||||
UserId = userId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Repo"></param>
|
|
||||||
public class RemoveEnvelopeReceiverCommandHandler(
|
|
||||||
IRepository<EnvelopeReceiver> Repo,
|
|
||||||
IRepository<DocReceiverElement> ElementRepo,
|
|
||||||
IRepository<ElementAnnotation> AnnotRepo) : IRequestHandler<RemoveEnvelopeReceiverCommand>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="NotImplementedException"></exception>
|
|
||||||
public async Task Handle(RemoveEnvelopeReceiverCommand request, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var er = await Repo.Query
|
|
||||||
.Where(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId)
|
|
||||||
.Include(er => er.Envelope)
|
|
||||||
.SingleOrDefaultAsync(cancel)
|
|
||||||
?? throw new NotFoundException();
|
|
||||||
|
|
||||||
if (er.Envelope is null)
|
|
||||||
throw new InvalidOperationException($"In {nameof(RemoveEnvelopeReceiverCommandHandler)} EnvelopeReceiver.Envelope is null." +
|
|
||||||
$"This should not happen." +
|
|
||||||
$"EnvelopeId: {request.EnvelopeId}, ReceiverId: {request.ReceiverId}");
|
|
||||||
|
|
||||||
if (request.UserId is int userId && er.Envelope.UserId != userId)
|
|
||||||
throw new AuthorizationException("Cannot remove receiver from envelope that you do not own.");
|
|
||||||
|
|
||||||
if (er.Envelope.Status > EnvelopeStatus.EnvelopeSaved)
|
|
||||||
throw new ForbiddenException("Cannot remove receiver from envelope that is already processed.");
|
|
||||||
|
|
||||||
// 1. Delete all ElementAnnotations for this EnvelopeReceiver
|
|
||||||
await AnnotRepo.DeleteAsync(ann => ann.Element.Document.EnvelopeId == request.EnvelopeId && ann.Element.ReceiverId == request.ReceiverId, cancel);
|
|
||||||
|
|
||||||
// 2. Delete all DocReceiverElements for this EnvelopeReceiver
|
|
||||||
await ElementRepo.DeleteAsync(el => el.Document.EnvelopeId == request.EnvelopeId && el.ReceiverId == request.ReceiverId, cancel);
|
|
||||||
|
|
||||||
// 3. Delete the EnvelopeReceiver
|
|
||||||
await Repo.DeleteAsync(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId, cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,9 +16,5 @@ public class MappingProfile : Profile
|
|||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<Receiver, ReceiverDto>();
|
CreateMap<Receiver, ReceiverDto>();
|
||||||
|
|
||||||
CreateMap<EnvelopeReceiver, RecentReceiverSuggestionDto>()
|
|
||||||
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
|
|
||||||
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Receiver.EmailAddress));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,24 +21,7 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public record ReadEnvelopeReceiverSecretQuery
|
public record ReadEnvelopeReceiverSecretQuery
|
||||||
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
|
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
|
||||||
IRequest<EnvelopeReceiverSecretDto?>
|
IRequest<EnvelopeReceiverSecretDto?>;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public ReadEnvelopeReceiverSecretQuery() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeId"></param>
|
|
||||||
/// <param name="receiverId"></param>
|
|
||||||
public ReadEnvelopeReceiverSecretQuery(int envelopeId, int receiverId)
|
|
||||||
{
|
|
||||||
Envelope.Id = envelopeId;
|
|
||||||
Receiver.Id = receiverId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
|
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads recent receiver suggestions for envelope creation.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Sender scope is assigned via chain method to avoid exposing username mutation from API query binding.
|
|
||||||
/// </remarks>
|
|
||||||
public record ReadRecentReceiverSuggestionsQuery : IRequest<IReadOnlyList<RecentReceiverSuggestionDto>>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Sender username used for scoping suggestion results.
|
|
||||||
/// </summary>
|
|
||||||
internal string? Username { get; private init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Desired number of results.
|
|
||||||
/// </summary>
|
|
||||||
public int? Take { get; init; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a new query instance scoped to the specified sender username.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="username">Sender username from authenticated claims.</param>
|
|
||||||
/// <returns>Scoped query instance.</returns>
|
|
||||||
public ReadRecentReceiverSuggestionsQuery ForSender(string username)
|
|
||||||
{
|
|
||||||
return this with { Username = username };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles <see cref="ReadRecentReceiverSuggestionsQuery"/>.
|
|
||||||
/// </summary>
|
|
||||||
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository)
|
|
||||||
: IRequestHandler<ReadRecentReceiverSuggestionsQuery, IReadOnlyList<RecentReceiverSuggestionDto>>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves distinct recent receiver suggestions grouped by receiver email.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Suggestion query.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
/// <returns>Ordered list of recent receiver suggestions.</returns>
|
|
||||||
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var query = repository.Query
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(er => er.Receiver != null);
|
|
||||||
|
|
||||||
if (request.Username is not null)
|
|
||||||
query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username);
|
|
||||||
|
|
||||||
// 1) For each email, compute the latest usage timestamp.
|
|
||||||
var latestPerEmail = query
|
|
||||||
.GroupBy(er => er.Receiver!.EmailAddress)
|
|
||||||
.Select(g => new
|
|
||||||
{
|
|
||||||
EmailAddress = g.Key,
|
|
||||||
LastUsedAt = g.Max(er => er.AddedWhen),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2) Join back to rows that match the latest timestamp per email.
|
|
||||||
var latestRows =
|
|
||||||
from er in query
|
|
||||||
join latest in latestPerEmail
|
|
||||||
on new { EmailAddress = er.Receiver!.EmailAddress, LastUsedAt = er.AddedWhen }
|
|
||||||
equals new { latest.EmailAddress, latest.LastUsedAt }
|
|
||||||
select new
|
|
||||||
{
|
|
||||||
EmailAddress = er.Receiver!.EmailAddress,
|
|
||||||
FullName = er.Name,
|
|
||||||
LastUsedAt = er.AddedWhen,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 3) Tie-break: if multiple rows share the same latest timestamp for an email,
|
|
||||||
// pick a deterministic name via Max(FullName), then order by most recent usage.
|
|
||||||
var suggestions = latestRows
|
|
||||||
.GroupBy(x => new { x.EmailAddress, x.LastUsedAt })
|
|
||||||
.Select(g => new RecentReceiverSuggestionDto
|
|
||||||
{
|
|
||||||
EmailAddress = g.Key.EmailAddress,
|
|
||||||
FullName = g.Max(x => x.FullName) ?? string.Empty,
|
|
||||||
LastUsedAt = g.Key.LastUsedAt,
|
|
||||||
})
|
|
||||||
.OrderByDescending(x => x.LastUsedAt)
|
|
||||||
.ThenBy(x => x.EmailAddress);
|
|
||||||
|
|
||||||
return request.Take is int take
|
|
||||||
? await suggestions.Take(take).ToListAsync(cancel)
|
|
||||||
: await suggestions.ToListAsync(cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using AutoMapper;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -17,14 +18,17 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
|
|||||||
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
||||||
{
|
{
|
||||||
private readonly IRepository<EnvelopeType> _repository;
|
private readonly IRepository<EnvelopeType> _repository;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="repository"></param>
|
/// <param name="repository"></param>
|
||||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
|
/// <param name="mapper"></param>
|
||||||
|
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -35,28 +39,7 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
|
var types = await _repository.Query.ToListAsync(cancellationToken);
|
||||||
// Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
|
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
|
||||||
return await _repository.Query
|
|
||||||
.AsNoTracking()
|
|
||||||
.Select(t => new EnvelopeTypeDto
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
Title = t.Title ?? string.Empty,
|
|
||||||
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
|
||||||
ExpiresDays = t.ExpiresDays,
|
|
||||||
CertificationType = t.CertificationType,
|
|
||||||
UseAccessCode = t.UseAccessCode,
|
|
||||||
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
|
||||||
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
|
||||||
AddedWhen = t.AddedWhen,
|
|
||||||
ChangedWhen = t.ChangedWhen,
|
|
||||||
ExpiresWarningDays = t.ExpiresWarningDays,
|
|
||||||
SendReminderEmails = t.SendReminderEmails,
|
|
||||||
FirstReminderDays = t.FirstReminderDays,
|
|
||||||
ReminderIntervalDays = t.ReminderIntervalDays,
|
|
||||||
ContractType = t.ContractType
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user