Compare commits
1 Commits
120485ee8d
...
refactor/f
| Author | SHA1 | Date | |
|---|---|---|---|
| 8db72037e0 |
@@ -1,946 +0,0 @@
|
|||||||
# EnvelopeGenerator — Copilot Context Notes (English)
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
A digital document signing system. Senders upload PDFs and place signature annotation fields via PSPDFKit (EnvelopeGenerator.Web). Receivers open the document in a Blazor WASM viewer, confirm each signature field via a checkbox overlay, draw/type/upload their signature, and export the stamped PDF.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Solution Structure
|
|
||||||
|
|
||||||
| Project | Target | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Receiver auth (cookie), annotation reading, PDF serving. |
|
|
||||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | Blazor WebAssembly. Receiver UI. YARP proxies API calls. |
|
|
||||||
| `EnvelopeGenerator.Web` | net7/8/9 | Razor Pages. Sender UI + PSPDFKit annotation placement. |
|
|
||||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. |
|
|
||||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
|
||||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
|
||||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities. NOT used in ReceiverUI flow. |
|
|
||||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
|
||||||
| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `ReceiverUI/Pages/EnvelopeViewer.razor` | **NEW** PDF.js-based viewer (`/envelope/{key}`). Replaces ReportViewer.razor. Simple read-only PDF viewing with zoom/navigation. |
|
|
||||||
| `ReceiverUI/Pages/ReportViewer.razor` | **LEGACY** DevExpress-based signing page (`/receiver/{key}`). Still used for signature workflow. Will be deprecated. |
|
|
||||||
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | **NEW** PDF.js wrapper: rendering, zoom, pagination, mouse wheel control. |
|
|
||||||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | JS: checkbox overlay, signature pad (draw/type/image). |
|
|
||||||
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | **NEW** Styles for EnvelopeViewer.razor (external CSS, not inline). |
|
|
||||||
| `ReceiverUI/wwwroot/fake-data/annotations.json` | Dev-mode fake annotations (YARP proxy target). |
|
|
||||||
| `ReceiverUI/Models/AnnotationDto.cs` | Annotation position model. All properties non-nullable. |
|
|
||||||
| `ReceiverUI/Services/AnnotationService.cs` | Fetches `List<AnnotationDto>` from API or fake-data. |
|
|
||||||
| `ReceiverUI/Services/DocumentService.cs` | Fetches PDF bytes from API. |
|
|
||||||
| `ReceiverUI/Services/AuthService.cs` | Manages receiver session cookie. |
|
|
||||||
| `API/Controllers/AnnotationController.cs` | GET `api/Annotation/{key}` ? annotation list. |
|
|
||||||
| `API/Controllers/DocumentController.cs` | GET `api/Document/{key}` ? PDF bytes. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SignatureDto / AnnotationDto — Coordinate System
|
|
||||||
|
|
||||||
**Database Storage Format:** INCHES (GdPicture14 native unit)
|
|
||||||
**Origin:** Top-left corner of page
|
|
||||||
**Axes:** X increases rightward, Y increases downward
|
|
||||||
|
|
||||||
### Source Evidence (VB.NET Legacy Code)
|
|
||||||
|
|
||||||
```vb
|
|
||||||
' From: EnvelopeGenerator.Form/frmFieldEditor.vb
|
|
||||||
' GdPicture14.Annotations.AnnotationStickyNote
|
|
||||||
|
|
||||||
'Breite und Höhe in Inches (4,5*5cm)
|
|
||||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' 1.77 inches = 4.5cm
|
|
||||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' 1.96 inches = 5cm
|
|
||||||
|
|
||||||
Sub LoadAnnotation(pElement As Signature, ...)
|
|
||||||
oAnnotation.Left = CSng(pElement.X) ' Direct assignment ? INCHES
|
|
||||||
oAnnotation.Top = CSng(pElement.Y)
|
|
||||||
oAnnotation.Width = CSng(pElement.Width)
|
|
||||||
oAnnotation.Height = CSng(pElement.Height)
|
|
||||||
End Sub
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conversion Formulas
|
|
||||||
|
|
||||||
```
|
|
||||||
Inches ? DevExpress (DX): x_DX = x_inches * 100.0
|
|
||||||
y_DX = y_inches * 100.0
|
|
||||||
|
|
||||||
Inches ? PDF Points: x_pt = x_inches * 72.0
|
|
||||||
y_pt = x_inches * 72.0
|
|
||||||
|
|
||||||
Inches ? PDF.js Canvas: normalize to [0,1], then scale to pixels
|
|
||||||
x_norm = x_inches / pageWidth_inches
|
|
||||||
y_norm = y_inches / pageHeight_inches
|
|
||||||
x_px = x_norm * canvasWidth * scale * dpr
|
|
||||||
y_px = y_norm * canvasHeight * scale * dpr
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Comparison Table
|
|
||||||
|
|
||||||
| System | Unit | Origin | Conversion from INCHES |
|
|
||||||
|--------|------|--------|------------------------|
|
|
||||||
| **GdPicture14** (Source) | **Inches** | Top-left | Database format (no conversion) |
|
|
||||||
| DevExpress (LEGACY) | 1/100 inch (DX) | Top-left | `x_DX = x_inches * 100` |
|
|
||||||
| PDF.js (NEW) | Pixels | Top-left | `normalize ? scale` |
|
|
||||||
| PDF Points (iText7) | Points (1/72") | **Bottom-left** | `x_pt = x_inches * 72` + Y-flip |
|
|
||||||
| PSPDFKit (Web) | Points (1/72") | Top-left | `x_pt = x_inches * 72` |
|
|
||||||
|
|
||||||
**A4 Page Dimensions:**
|
|
||||||
- Width: 8.27 inches = 595 points = 827 DX units
|
|
||||||
- Height: 11.69 inches = 842 points = 1169 DX units
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer
|
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
|
||||||
**Purpose:** Modern, high-performance PDF viewing without signing functionality.
|
|
||||||
**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
**Blazor Component (`EnvelopeViewer.razor`):**
|
|
||||||
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
|
|
||||||
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
|
||||||
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
|
|
||||||
- Quality settings loaded from `appsettings.json` via `IOptions<PdfViewerOptions>`
|
|
||||||
- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence)
|
|
||||||
- CSS externalized to `envelope-viewer.css`
|
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
|
||||||
```javascript
|
|
||||||
window.pdfViewer = {
|
|
||||||
qualityOptions, // Configurable from appsettings.json
|
|
||||||
setQualityOptions(options), // Dynamic quality update
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
|
||||||
renderPage(num),
|
|
||||||
renderThumbnail(pageNum, canvasId),
|
|
||||||
attachWheelEvent(), // Ctrl+Wheel zoom (configurable step)
|
|
||||||
zoomIn(), zoomOut(), // Configurable step percentage
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Options (`PdfViewerOptions.cs`):**
|
|
||||||
```csharp
|
|
||||||
public class PdfViewerOptions {
|
|
||||||
public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5
|
|
||||||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
|
||||||
public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0
|
|
||||||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
|
||||||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
|
||||||
public bool EnableSmoothZoom { get; set; } = true;
|
|
||||||
public int ZoomTransitionDuration { get; set; } = 150; // ms
|
|
||||||
public double RenderingOpacity { get; set; } = 0.85; // 0.0-1.0
|
|
||||||
public int ThumbnailRenderDelay { get; set; } = 50; // ms
|
|
||||||
public int ZoomStepPercentage { get; set; } = 5; // 1-50%
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration (appsettings.json)
|
|
||||||
|
|
||||||
**Location:** `EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"PdfViewer": {
|
|
||||||
"ThumbnailBaseScale": 0.75,
|
|
||||||
"ThumbnailEnableHiDPI": true,
|
|
||||||
"ThumbnailMaxDPR": 2.0,
|
|
||||||
"MainCanvasEnableHiDPI": true,
|
|
||||||
"MainCanvasMaxDPR": 2.0,
|
|
||||||
"EnableSmoothZoom": true,
|
|
||||||
"ZoomTransitionDuration": 150,
|
|
||||||
"RenderingOpacity": 0.85,
|
|
||||||
"ThumbnailRenderDelay": 50,
|
|
||||||
"ZoomStepPercentage": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:** Edit file ? F5 (browser refresh) ? new settings applied
|
|
||||||
|
|
||||||
**Presets:**
|
|
||||||
- **High Quality**: `ThumbnailBaseScale: 1.0, MaxDPR: 3.0` (powerful devices)
|
|
||||||
- **Balanced**: Default values (recommended)
|
|
||||||
- **Performance**: `ThumbnailBaseScale: 0.5, EnableHiDPI: false` (mobile/low-end)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
1. **HiDPI/Retina Support** ? 4x quality on Retina displays
|
|
||||||
2. **Configurable Quality** ? All parameters in appsettings.json
|
|
||||||
3. **Unlimited Zoom** ? 50%-300%, configurable step (default 5%)
|
|
||||||
4. **Global Ctrl+Wheel Zoom** ? Works anywhere on page
|
|
||||||
5. **Thumbnail Sidebar** ? Resizable (150-400px), high-quality rendering
|
|
||||||
6. **Smooth Transitions** ? Configurable fade effect
|
|
||||||
7. **Responsive Design** ? Desktop/mobile adaptive layout
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
1. **Unlimited Zoom:**
|
|
||||||
- Canvas size not restricted by `max-width`
|
|
||||||
- Frame stays fixed, scroll bars appear automatically
|
|
||||||
- `text-align: center` for small sizes, full scroll for zoomed views
|
|
||||||
|
|
||||||
2. **Global Mouse Wheel Zoom:**
|
|
||||||
- Event listener on `document.body` (works anywhere on page)
|
|
||||||
- `Ctrl + Mouse Wheel` triggers `zoomIn()`/`zoomOut()`
|
|
||||||
- Calls `dotNetReference.invokeMethodAsync('OnZoomChanged', scale)` to update Blazor UI
|
|
||||||
- `{ passive: false }` to enable `preventDefault()`
|
|
||||||
|
|
||||||
3. **Render Task Cancellation:**
|
|
||||||
- Stores `currentRenderTask` to cancel previous render if new one starts
|
|
||||||
- Catches `RenderingCancelledException` to avoid console errors
|
|
||||||
- Queue system (`pageNumPending`) for rapid page changes
|
|
||||||
|
|
||||||
4. **Thumbnail Sidebar:**
|
|
||||||
- Left panel with page previews (sequential rendering, 50ms delay)
|
|
||||||
- Click to navigate to specific page
|
|
||||||
- Active page highlighted with gradient border
|
|
||||||
- No header/title (maximizes thumbnail space)
|
|
||||||
- Toggle button in toolbar to show/hide
|
|
||||||
|
|
||||||
5. **Resizable Splitter:**
|
|
||||||
- 4px draggable divider between thumbnails and canvas
|
|
||||||
- Min width: 150px, Max width: 400px
|
|
||||||
- Visual feedback: gradient on hover/active
|
|
||||||
- User preference saved to localStorage (`envelopeViewer_thumbnailWidth`)
|
|
||||||
- Global mouse events (works anywhere during drag)
|
|
||||||
- `col-resize` cursor (?) for intuitive UX
|
|
||||||
|
|
||||||
6. **Flex Layout:**
|
|
||||||
- Thumbnails and canvas in same container (`.pdf-frame`)
|
|
||||||
- `display: flex, flex-direction: row, align-items: stretch`
|
|
||||||
- Perfect vertical alignment (same top/bottom position)
|
|
||||||
- Responsive: column layout on mobile (<768px)
|
|
||||||
|
|
||||||
7. **Responsive Design:**
|
|
||||||
- Desktop: 90% width, 1200px max
|
|
||||||
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
|
|
||||||
- Adaptive padding and font sizes
|
|
||||||
|
|
||||||
### Initialization Flow
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
OnInitializedAsync():
|
|
||||||
1. Fetch PDF bytes from DocumentService
|
|
||||||
2. Convert to base64 data URL
|
|
||||||
3. Set _isLoading = false
|
|
||||||
|
|
||||||
OnAfterRenderAsync(firstRender):
|
|
||||||
1. Load saved thumbnail width from localStorage
|
|
||||||
2. Create DotNetObjectReference
|
|
||||||
3. Send PdfViewerOptions to JavaScript
|
|
||||||
4. Initialize PDF.js viewer
|
|
||||||
5. Attach splitter resize listeners
|
|
||||||
6. Render thumbnails sequentially (configurable delay)
|
|
||||||
```
|
|
||||||
|
|
||||||
**User Interactions:**
|
|
||||||
- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage
|
|
||||||
- Pages: Buttons/Input/Thumbnails ? navigate
|
|
||||||
- Sidebar: Toggle button ? show/hide thumbnails
|
|
||||||
- Splitter: Drag ? resize sidebar (150-400px)
|
|
||||||
|
|
||||||
**Cleanup:**
|
|
||||||
```csharp
|
|
||||||
DisposeAsync():
|
|
||||||
- Dispose PDF.js viewer
|
|
||||||
- Detach event listeners
|
|
||||||
- Dispose DotNetObjectReference
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Key Differences from ReportViewer
|
|
||||||
|
|
||||||
| Feature | EnvelopeViewer (NEW) | ReportViewer (LEGACY) |
|
|
||||||
|---------|----------------------|------------------------|
|
|
||||||
| Technology | PDF.js + Canvas | DevExpress XtraReports |
|
|
||||||
| Route | `/envelope/{key}` | `/receiver/{key}` |
|
|
||||||
| Purpose | Read-only viewing | Signature workflow |
|
|
||||||
| Dependencies | PDF.js CDN | DevExpress NuGet packages |
|
|
||||||
| Zoom | Unlimited, smooth | Report viewer default |
|
|
||||||
| Mouse Wheel | Custom Ctrl+Wheel | Browser default |
|
|
||||||
| File Size | Minimal (JS + CSS) | Heavy (DX libs) |
|
|
||||||
| Maintenance | Simple, standard web | Complex, vendor-specific |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ReceiverUI Signing Flow (ReportViewer.razor — LEGACY)
|
|
||||||
|
|
||||||
### On Load (`OnInitializedAsync`)
|
|
||||||
1. `AuthService.CheckEnvelopeAccessAsync` ? redirect to login if unauthorized
|
|
||||||
2. `AnnotationService.GetAnnotationsAsync` ? fills `_annotations`
|
|
||||||
3. `DocumentService.GetDocumentAsync` ? fills `_basePdfBytes` (real mode)
|
|
||||||
4. `BuildFreshBaseReport()` ? `XtraReport` for `DxReportViewer`
|
|
||||||
|
|
||||||
### Signature Popup
|
|
||||||
- Tabs: Draw / Text / Image
|
|
||||||
- Fields: full name (required), position (optional), place (required)
|
|
||||||
- Saved to `_capturedSignature` record
|
|
||||||
- If annotations exist ? popup closes ? JS checkbox overlays installed
|
|
||||||
|
|
||||||
### JS Checkbox Overlay (`receiver-signature.js`)
|
|
||||||
- `receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)`
|
|
||||||
- One `.annot-sig-cb-wrapper` div per annotation, absolutely positioned over viewer scroll container
|
|
||||||
- Position: `left = pageRect.left + ann.x * scaleX`, `top = pageRect.top + ann.y * scaleY`
|
|
||||||
- `scaleX = pagePixelWidth / 827`, `scaleY = pagePixelHeight / 1169`
|
|
||||||
- Click ? `dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)`
|
|
||||||
|
|
||||||
### Apply Signatures ("Unterschriften anwenden" — `SubmitSignaturesAsync`)
|
|
||||||
|
|
||||||
**Real PDF mode (`_basePdfBytes` is set):**
|
|
||||||
- Calls `StampSignaturesOnPdf` using **iText7** directly on PDF bytes
|
|
||||||
- Coordinate conversion: `xPt = ann.X * (72/100)`, `yPt = pageHeight - ann.Y * (72/100) - sigHeight` (Y flip: DX top-down ? PDF bottom-up)
|
|
||||||
- Returns stamped bytes ? loaded into new `XtraReport` with `XRPdfContent`
|
|
||||||
- Viewer refreshed with `ViewerKey++`
|
|
||||||
|
|
||||||
**Dev/fake mode (`_basePdfBytes` is null):**
|
|
||||||
- Falls back to `AddSignatureAtAnnotation` (XtraReports DetailBand + BeforePrint counter)
|
|
||||||
- This is intentionally left as a dev fallback only
|
|
||||||
|
|
||||||
### Export
|
|
||||||
- `reportViewer.ExportToAsync(ExportFormat.Pdf)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## StampSignaturesOnPdf — iText7 Implementation
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Located in ReportViewer.razor @code section
|
|
||||||
// Called by SubmitSignaturesAsync when _basePdfBytes is available
|
|
||||||
|
|
||||||
static byte[] StampSignaturesOnPdf(
|
|
||||||
byte[] sourcePdfBytes, byte[] signatureImageBytes,
|
|
||||||
string signerFullName, string signerPosition, string signaturePlace,
|
|
||||||
IReadOnlyList<AnnotationDto> annotations)
|
|
||||||
{
|
|
||||||
// Opens PDF with PdfReader/PdfWriter
|
|
||||||
// For each annotation:
|
|
||||||
// pageNum = ann.Page (clamped to totalPages)
|
|
||||||
// xPt = ann.X * (72f/100f)
|
|
||||||
// imgBottomY = pageHeight - ann.Y * (72f/100f) - sigHeightPt ? Y-axis flip
|
|
||||||
// PdfCanvas.AddImageFittedIntoRectangle(imageData, rect, false)
|
|
||||||
// Separator line at bottom of image
|
|
||||||
// Canvas text block below separator (font: Helvetica 7pt, color: RGB(73,80,87))
|
|
||||||
// Returns stamped byte[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BuildFreshBaseReport()
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Real PDF mode:
|
|
||||||
var report = new XtraReport();
|
|
||||||
var detail = new DetailBand();
|
|
||||||
report.Bands.Add(detail);
|
|
||||||
detail.Controls.Add(new XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
|
||||||
return report;
|
|
||||||
|
|
||||||
// Dev/fake mode: returns pre-built report from ReportStorage
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NuGet Packages in ReceiverUI
|
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `DevExpress.Blazor.Reporting.Viewer` | 25.2.3 | DxReportViewer (LEGACY, used in ReportViewer.razor) |
|
|
||||||
| `DevExpress.Blazor.PdfViewer` | 25.2.3 | PDF viewer (not used in EnvelopeViewer) |
|
|
||||||
| `DevExpress.Drawing.Skia` | 25.2.3 | Drawing backend |
|
|
||||||
| `itext` | 8.0.5 | PDF stamping (iText7) |
|
|
||||||
| `SkiaSharp.*` | 3.119.1 | WASM native rendering |
|
|
||||||
|
|
||||||
**External CDN (EnvelopeViewer):**
|
|
||||||
- PDF.js 3.11.174 (via `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`)
|
|
||||||
- PDF.js Worker (`pdf.worker.min.js`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
|
||||||
|
|
||||||
| Mistake | Why Wrong | Session |
|
|
||||||
|---|---|---|
|
|
||||||
| `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong | 4 |
|
|
||||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages | 8 |
|
|
||||||
| `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` | 5 |
|
|
||||||
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM | — |
|
|
||||||
| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI | — |
|
|
||||||
| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly | 10 |
|
|
||||||
| **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | **11** |
|
|
||||||
| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | **11** |
|
|
||||||
| **Toolbar: Complex left/center/right layout** | **User wants simple horizontal layout; failed multiple times to implement** | **11** |
|
|
||||||
| **Zoom label: Badge style (gradient/border/padding)** | **Over-designed; user prefers simple text label** | **11** |
|
|
||||||
| **Attempting to "improve" simple designs** | **User requests simplicity; AI keeps over-engineering** | **11** |
|
|
||||||
| **Ignoring explicit "revert" instructions** | **User said revert toolbar, AI tried to fix CSS instead of reverting HTML structure** | **11** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DevExpress Article (2023-08-28) — Why It Does NOT Apply
|
|
||||||
|
|
||||||
The article describes **X.509 cryptographic digital signatures** via `PdfDocumentSigner` + `Pkcs7Signer`.
|
|
||||||
Our use case is **visual/image stamping** at specific page coordinates — different problem, different API.
|
|
||||||
`XRPdfSignature` in the article requires pre-placed fields in the report designer, not runtime coordinates.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Layout Architecture (EnvelopeViewer)
|
|
||||||
|
|
||||||
### HTML Structure
|
|
||||||
```html
|
|
||||||
<div class="pdf-viewer-container">
|
|
||||||
<div class="pdf-toolbar">
|
|
||||||
<!-- Zoom, page navigation, thumbnail toggle -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-frame">
|
|
||||||
@if (_showThumbnails) {
|
|
||||||
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
|
||||||
<!-- Page previews -->
|
|
||||||
</div>
|
|
||||||
<div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
|
|
||||||
<!-- Resizable divider -->
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="pdf-canvas-wrapper">
|
|
||||||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### CSS Flexbox Layout
|
|
||||||
```css
|
|
||||||
.pdf-frame {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row; /* Side-by-side */
|
|
||||||
align-items: stretch; /* Same height */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-thumbnails {
|
|
||||||
flex-shrink: 0; /* Fixed width */
|
|
||||||
width: 260px; /* Dynamic via inline style */
|
|
||||||
border-right: none; /* Seamless join with splitter */
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-splitter {
|
|
||||||
width: 4px;
|
|
||||||
cursor: col-resize;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-canvas-wrapper {
|
|
||||||
flex: 1; /* Fill remaining space */
|
|
||||||
overflow: auto; /* Scrollable for zoom */
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resizable Splitter Workflow
|
|
||||||
```
|
|
||||||
1. User hovers splitter ? cursor: col-resize (?)
|
|
||||||
2. Mouse down ? OnSplitterMouseDown(e)
|
|
||||||
- _isResizing = true
|
|
||||||
- Store start position (clientX) and width
|
|
||||||
- Add 'resizing' class to body (prevent text selection)
|
|
||||||
- Call pdfViewer.startResize()
|
|
||||||
|
|
||||||
3. Mouse move (global) ? OnSplitterMouseMove(clientX)
|
|
||||||
- Calculate delta = clientX - startX
|
|
||||||
- newWidth = startWidth + delta
|
|
||||||
- Clamp to 150-400px range
|
|
||||||
- Update _thumbnailWidth
|
|
||||||
- StateHasChanged() for reactive UI
|
|
||||||
|
|
||||||
4. Mouse up (global) ? OnSplitterMouseUp()
|
|
||||||
- _isResizing = false
|
|
||||||
- Remove 'resizing' class
|
|
||||||
- Save to localStorage("envelopeViewer_thumbnailWidth")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Responsive Behavior
|
|
||||||
- **Desktop (>768px)**: Flex row, side-by-side
|
|
||||||
- **Mobile (?768px)**: Flex column, thumbnails on top
|
|
||||||
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Signature Buttons in EnvelopeViewer — Interactive Overlay System
|
|
||||||
|
|
||||||
**Purpose:** Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database.
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
**Blazor Component (`EnvelopeViewer.razor`):**
|
|
||||||
```csharp
|
|
||||||
IReadOnlyList<SignatureDto> _signatures = [];
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
var signatures = await SignatureService.GetAsync(EnvelopeKey);
|
|
||||||
_signatures = signatures.Convert(UnitOfLength.Point); // INCHES ? POINTS
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RenderSignatureButtonsAsync() {
|
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons",
|
|
||||||
_signatures, _currentPage, _dotNetRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public void OnSignatureButtonClick(int signatureId) {
|
|
||||||
Console.WriteLine($"Signature #{signatureId} signed");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
|
||||||
```javascript
|
|
||||||
renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
|
||||||
this.clearSignatureButtons(); // Remove old buttons
|
|
||||||
|
|
||||||
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
|
|
||||||
const signatureLayer = document.getElementById('pdf-signature-layer');
|
|
||||||
|
|
||||||
pageSignatures.forEach(sig => {
|
|
||||||
// Convert POINTS to display pixels
|
|
||||||
const xPx = sig.x * this.scale; // sig.x already in PDF POINTS
|
|
||||||
const yPx = sig.y * this.scale;
|
|
||||||
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.className = 'signature-button';
|
|
||||||
button.style.left = `${xPx}px`;
|
|
||||||
button.style.top = `${yPx}px`;
|
|
||||||
button.style.width = '150px';
|
|
||||||
button.style.height = '60px';
|
|
||||||
|
|
||||||
// German text + pen icon
|
|
||||||
button.innerHTML = `
|
|
||||||
<div>Unterschreiben</div>
|
|
||||||
<svg>...</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
signatureLayer.appendChild(button);
|
|
||||||
this.signatureButtons.push(button);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSignatureButtons() {
|
|
||||||
this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn));
|
|
||||||
this.signatureButtons = [];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**HTML Structure:**
|
|
||||||
```html
|
|
||||||
<div class="pdf-page-container">
|
|
||||||
<canvas id="pdf-canvas"></canvas>
|
|
||||||
<div id="pdf-text-layer"></div>
|
|
||||||
<div id="pdf-signature-layer"></div> <!-- NEW -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**CSS (`envelope-viewer.css`):**
|
|
||||||
```css
|
|
||||||
.pdf-signature-layer {
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0; right: 0; bottom: 0;
|
|
||||||
overflow: visible;
|
|
||||||
pointer-events: none; /* Pass clicks through to canvas */
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-button {
|
|
||||||
pointer-events: auto; /* Re-enable for buttons */
|
|
||||||
position: absolute;
|
|
||||||
width: 150px; height: 60px;
|
|
||||||
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
/* Hover: scale(1.05), shadow, darker gradient */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rendering Triggers
|
|
||||||
|
|
||||||
1. **Initial Load:** After PDF renders (`OnAfterRenderAsync`)
|
|
||||||
2. **Page Change:** `NextPage()`, `PreviousPage()`, `GoToPageFromThumbnail()`
|
|
||||||
3. **Zoom Change:** `OnZoomChanged()` (re-calculates pixel positions)
|
|
||||||
|
|
||||||
### Coordinate Conversion Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Database (INCHES)
|
|
||||||
? SignatureService.GetAsync()
|
|
||||||
SignatureDto.X/Y (INCHES)
|
|
||||||
? .Convert(UnitOfLength.Point)
|
|
||||||
SignatureDto.X/Y (PDF POINTS, × 72)
|
|
||||||
? JavaScript
|
|
||||||
Display Pixels (× this.scale)
|
|
||||||
? CSS
|
|
||||||
button.style.left/top
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example Calculation:**
|
|
||||||
- Database: `X = 1.5 inches, Y = 2.0 inches`
|
|
||||||
- After conversion: `X = 108 points, Y = 144 points` (× 72)
|
|
||||||
- At scale 1.5: `X = 162px, Y = 216px`
|
|
||||||
- Button positioned at `left: 162px, top: 216px`
|
|
||||||
|
|
||||||
### Button Design
|
|
||||||
|
|
||||||
**Visual Spec:**
|
|
||||||
- **Size:** 150px × 60px
|
|
||||||
- **Background:** Purple gradient `#4F46E5` ? `#4338CA` (hover: darker)
|
|
||||||
- **Text:** "Unterschreiben" (18px, bold, white)
|
|
||||||
- **Icon:** Pen SVG (24px white)
|
|
||||||
- **Effects:**
|
|
||||||
- Hover: `scale(1.05)` + shadow `0 4px 12px rgba(79, 70, 229, 0.4)`
|
|
||||||
- Active: `scale(0.98)`
|
|
||||||
- Focus: `2px solid #7e22ce` outline
|
|
||||||
|
|
||||||
**Accessibility:**
|
|
||||||
- `tabindex="0"` for keyboard navigation
|
|
||||||
- Focus outline for keyboard users
|
|
||||||
- Click handler with semantic button element
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13-14)
|
|
||||||
|
|
||||||
**IMPORTANT: iText7 NOT USED in EnvelopeViewer**
|
|
||||||
- **Reason:** GPL license incompatibility (requires source code sharing)
|
|
||||||
- **Alternative:** Client-side signature overlay system (HTML + Canvas API)
|
|
||||||
- **Export:** Signatures are visual overlays only, NOT stamped on PDF bytes
|
|
||||||
- **Future:** Consider PSPDFKit or commercial PDF library for actual PDF stamping
|
|
||||||
|
|
||||||
### Signature Data Structure
|
|
||||||
|
|
||||||
**Captured Signature (`SignatureCaptureDto`):**
|
|
||||||
```csharp
|
|
||||||
// Model: EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs
|
|
||||||
public sealed record SignatureCaptureDto
|
|
||||||
{
|
|
||||||
public required string DataUrl { get; init; } // base64 PNG: "data:image/png;base64,iVBORw0KG..."
|
|
||||||
public required string FullName { get; init; } // Required: "Max Mustermann"
|
|
||||||
public string Position { get; init; } = string.Empty; // Optional: "Geschäftsführer"
|
|
||||||
public required string Place { get; init; } // Required: "Berlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage in components:
|
|
||||||
SignatureCaptureDto? _capturedSignature;
|
|
||||||
|
|
||||||
// Initialization with object initializer (required properties):
|
|
||||||
_capturedSignature = new SignatureCaptureDto
|
|
||||||
{
|
|
||||||
DataUrl = signatureDataUrl,
|
|
||||||
FullName = _signerFullName.Trim(),
|
|
||||||
Position = _signerPosition.Trim(),
|
|
||||||
Place = _signaturePlace.Trim()
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Applied Signature (HTML Overlay):**
|
|
||||||
```html
|
|
||||||
<div class="applied-signature" data-signature-id="42" style="left: 162px; top: 216px;">
|
|
||||||
<img src="data:image/png;base64,..." /> <!-- Signature image (max 70px height) -->
|
|
||||||
<div style="border-top: 1px solid #495057;"></div> <!-- Separator line -->
|
|
||||||
<div style="font-size: 9px; color: #495057;">
|
|
||||||
<strong>Max Mustermann</strong> <!-- Name (bold, #212529) -->
|
|
||||||
<br>Geschäftsführer <!-- Position (optional) -->
|
|
||||||
<br>Berlin, 26.01.2025 <!-- Place, Date (dd.MM.yyyy) -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complete Workflow (Session 14 Update)
|
|
||||||
|
|
||||||
**Step 1: Page Load & Automatic Popup**
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
// ... load PDF and signatures ...
|
|
||||||
|
|
||||||
// Open signature popup automatically
|
|
||||||
_activeSignatureTab = SignatureTabDraw;
|
|
||||||
_signaturePopupVisible = true;
|
|
||||||
_popupValidationMessage = null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Opens automatically on page load (no manual trigger needed)
|
|
||||||
- Cannot be closed manually (no X button, ESC disabled, no outside-click)
|
|
||||||
- User MUST create signature before viewing PDF
|
|
||||||
|
|
||||||
**Step 2: Signature Creation Popup (DxPopup)**
|
|
||||||
|
|
||||||
**Tabs:**
|
|
||||||
1. **Zeichnen (Draw):** Canvas-based signature pad (`receiver-signature.js`)
|
|
||||||
- Touch-friendly: `touch-action: none`
|
|
||||||
- Line width: 2.5px, black (`#111`)
|
|
||||||
- Canvas: 560×180px, rounded corners, shadow
|
|
||||||
|
|
||||||
2. **Text:** Type signature with font selection
|
|
||||||
- Fonts: Brush Script, Segoe Script, Lucida Handwriting, Comic Sans, Cursive
|
|
||||||
- Real-time preview on canvas
|
|
||||||
|
|
||||||
3. **Bild (Image):** Upload PNG/JPG/WebP
|
|
||||||
- File input with preview
|
|
||||||
- Auto-resize to fit canvas
|
|
||||||
|
|
||||||
**Required Fields:**
|
|
||||||
- ? **Vor- und Nachname** (Full Name) — Red asterisk `*`
|
|
||||||
- ? **Ort** (Place) — Red asterisk `*`
|
|
||||||
- ? **Position** — Optional, gray "(optional)" label
|
|
||||||
|
|
||||||
**Validation:**
|
|
||||||
```csharp
|
|
||||||
async Task SaveSignatureAsync() {
|
|
||||||
if (string.IsNullOrWhiteSpace(_signerFullName)) {
|
|
||||||
_popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(_signaturePlace)) {
|
|
||||||
_popupValidationMessage = "Bitte geben Sie den Ort ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
|
||||||
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
|
||||||
_popupValidationMessage = "Die Unterschrift ist erforderlich.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to session state
|
|
||||||
_capturedSignature = new(signatureDataUrl, _signerFullName.Trim(),
|
|
||||||
_signerPosition.Trim(), _signaturePlace.Trim());
|
|
||||||
_signaturePopupVisible = false;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Design (Modern & Clean):**
|
|
||||||
- **Tabs:** Purple active state (`#4F46E5`), 3px bottom border
|
|
||||||
- **Inputs:** 2px solid border (`#e9ecef`), 6px border-radius, consistent padding
|
|
||||||
- **Canvas:** Light shadow (`0 1px 3px rgba(0,0,0,0.1)`), rounded corners
|
|
||||||
- **Buttons:**
|
|
||||||
- **Erneuern:** Outline secondary with refresh icon
|
|
||||||
- **Speichern:** Purple gradient + checkmark icon + shadow
|
|
||||||
- **Error:** Red left border (`4px solid #dc3545`), light red background (`#fee`)
|
|
||||||
|
|
||||||
**Step 3: PDF Viewing with Signature Buttons**
|
|
||||||
|
|
||||||
After popup closes:
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
|
||||||
// ... PDF initialization ...
|
|
||||||
|
|
||||||
await RenderSignatureButtonsAsync(); // Render "Unterschreiben" buttons
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Buttons appear at signature field positions:**
|
|
||||||
- Purple gradient background
|
|
||||||
- "Unterschreiben" text + pen icon
|
|
||||||
- Hover: scale(1.05) + darker color
|
|
||||||
- Positioned using PDF POINTS ? display pixels conversion
|
|
||||||
|
|
||||||
**Step 4: Apply Signature (Click "Unterschreiben")**
|
|
||||||
|
|
||||||
**C# Handler:**
|
|
||||||
```csharp
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnSignatureButtonClick(int signatureId) {
|
|
||||||
if (_capturedSignature == null) return;
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature",
|
|
||||||
signatureId,
|
|
||||||
_capturedSignature.DataUrl,
|
|
||||||
_capturedSignature.FullName,
|
|
||||||
_capturedSignature.Position,
|
|
||||||
_capturedSignature.Place);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript Implementation:**
|
|
||||||
```javascript
|
|
||||||
async applySignature(signatureId, signatureDataUrl, fullName, position, place) {
|
|
||||||
// 1. Find and remove button
|
|
||||||
const button = this.signatureButtons.find(btn =>
|
|
||||||
btn.getAttribute('data-signature-id') == signatureId);
|
|
||||||
button.parentNode.removeChild(button);
|
|
||||||
|
|
||||||
// 2. Create signature container (German standard format)
|
|
||||||
const signatureContainer = document.createElement('div');
|
|
||||||
signatureContainer.className = 'applied-signature';
|
|
||||||
signatureContainer.style.position = 'absolute';
|
|
||||||
signatureContainer.style.left = button.style.left; // Same position as button
|
|
||||||
signatureContainer.style.top = button.style.top;
|
|
||||||
signatureContainer.style.width = '230px';
|
|
||||||
signatureContainer.style.backgroundColor = '#f8f9fa';
|
|
||||||
signatureContainer.style.border = '1px solid #dee2e6';
|
|
||||||
signatureContainer.style.borderRadius = '6px';
|
|
||||||
signatureContainer.style.padding = '12px';
|
|
||||||
|
|
||||||
// 3. Add signature image
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = signatureDataUrl;
|
|
||||||
img.style.width = '100%';
|
|
||||||
img.style.maxHeight = '70px';
|
|
||||||
img.style.objectFit = 'contain';
|
|
||||||
|
|
||||||
// 4. Add separator line (German standard)
|
|
||||||
const separator = document.createElement('div');
|
|
||||||
separator.style.borderTop = '1px solid #495057';
|
|
||||||
separator.style.marginTop = '6px';
|
|
||||||
separator.style.marginBottom = '8px';
|
|
||||||
|
|
||||||
// 5. Add text information
|
|
||||||
const today = new Date();
|
|
||||||
const dateStr = today.toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
|
||||||
}); // "26.01.2025"
|
|
||||||
|
|
||||||
const infoHtml = [
|
|
||||||
`<strong>${this.escapeHtml(fullName)}</strong>`,
|
|
||||||
position ? this.escapeHtml(position) : null,
|
|
||||||
`${this.escapeHtml(place)}, ${dateStr}`
|
|
||||||
].filter(x => x).join('<br>');
|
|
||||||
|
|
||||||
const info = document.createElement('div');
|
|
||||||
info.style.fontSize = '9px';
|
|
||||||
info.style.color = '#495057';
|
|
||||||
info.innerHTML = infoHtml;
|
|
||||||
|
|
||||||
// 6. Assemble and add to layer
|
|
||||||
signatureContainer.appendChild(img);
|
|
||||||
signatureContainer.appendChild(separator);
|
|
||||||
signatureContainer.appendChild(info);
|
|
||||||
|
|
||||||
document.getElementById('pdf-signature-layer').appendChild(signatureContainer);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**German Standard Layout:**
|
|
||||||
```
|
|
||||||
???????????????????????????????
|
|
||||||
? [Signature Image] ? ? Base64 PNG, max 70px height
|
|
||||||
? ?
|
|
||||||
??????????????????????????????? ? 1px separator (#495057)
|
|
||||||
? ?
|
|
||||||
? Max Mustermann (Bold) ? ? Name (font-weight: 600, #212529)
|
|
||||||
? Geschäftsführer ? ? Position (optional, normal weight)
|
|
||||||
? Berlin, 26.01.2025 ? ? Place, Date (dd.MM.yyyy)
|
|
||||||
? ?
|
|
||||||
???????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 5: Persistence & Re-rendering**
|
|
||||||
|
|
||||||
- **Zoom/Page Change:** Applied signatures re-render automatically
|
|
||||||
- **Session State:** `_capturedSignature` stored in Blazor component
|
|
||||||
- **Limitation:** Lost on page refresh (no server-side storage)
|
|
||||||
- **Future:** Export to PDF with actual byte stamping (requires non-GPL library)
|
|
||||||
|
|
||||||
**Security:**
|
|
||||||
```javascript
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text; // Browser auto-escapes
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Protects against XSS attacks from malicious input in Name/Position/Place fields.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Popup Design Specification
|
|
||||||
|
|
||||||
**DxPopup Properties:**
|
|
||||||
```razor
|
|
||||||
<DxPopup @bind-Visible="_signaturePopupVisible"
|
|
||||||
HeaderText="Unterschrift erstellen"
|
|
||||||
Width="620px"
|
|
||||||
MaxWidth="95vw"
|
|
||||||
ShowFooter="true" <!-- REQUIRED for buttons to appear -->
|
|
||||||
CloseOnOutsideClick="false"
|
|
||||||
ShowCloseButton="false" <!-- No X button -->
|
|
||||||
CloseOnEscape="false"> <!-- ESC disabled -->
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tab Design:**
|
|
||||||
- **Active Tab:** `border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;`
|
|
||||||
- **Inactive Tab:** `color: #6c757d;`
|
|
||||||
- **Tab Bar:** `border-bottom: 2px solid #e9ecef;`
|
|
||||||
|
|
||||||
**Input Styling:**
|
|
||||||
```css
|
|
||||||
input, select {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Canvas Styling:**
|
|
||||||
```css
|
|
||||||
canvas {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Button Styling:**
|
|
||||||
```css
|
|
||||||
/* Erneuern (Renew) */
|
|
||||||
.btn-outline-secondary {
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Speichern (Save) */
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Future Enhancement Required:**
|
|
||||||
- Replace iText7 with commercial PDF library (e.g., PSPDFKit, Syncfusion)
|
|
||||||
- Or use server-side stamping with Azure PDF Services
|
|
||||||
- Or accept GPL license and open-source the stamping code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
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.Documents.Queries;
|
|
||||||
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.Components.Forms;
|
|
||||||
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.GetReceiverSignatureOfReceiver();
|
|
||||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
|
||||||
|
|
||||||
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 docSignedNotification = await _mediator
|
|
||||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
|
||||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
|
||||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
|
||||||
|
|
||||||
await _mediator.PublishSafely(docSignedNotification, cancel);
|
|
||||||
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.GetReceiverSignatureOfReceiver();
|
|
||||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
|
||||||
var mail = User.GetReceiverMailOfReceiver();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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(Policy = AuthPolicy.SenderOrReceiver)]
|
|
||||||
[HttpPost("logout")]
|
|
||||||
public async Task<IActionResult> Logout()
|
|
||||||
{
|
|
||||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
|
||||||
Response.Cookies.Delete(authTokenKeys.Cookie);
|
|
||||||
else if (await this.IsUserInPolicyAsync(AuthPolicy.ReceiverOrReceiverTFA))
|
|
||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
else
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prüft, ob der Benutzer ein autorisiertes Token hat.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Sample request:
|
|
||||||
///
|
|
||||||
/// GET /api/auth
|
|
||||||
///
|
|
||||||
/// </remarks>
|
|
||||||
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
|
|
||||||
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
|
||||||
[HttpGet("check")]
|
|
||||||
[Authorize]
|
|
||||||
public IActionResult Check(string? role = null)
|
|
||||||
=> role is not null && !User.IsInRole(role)
|
|
||||||
? Unauthorized()
|
|
||||||
: Ok();
|
|
||||||
|
|
||||||
/// <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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
|
||||||
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]
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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.GetEnvelopeIdOfReceiver();
|
|
||||||
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
|
|
||||||
return receiverDoc.ByteData is byte[] receiverDocByte
|
|
||||||
? File(receiverDocByte, "application/octet-stream")
|
|
||||||
: NotFound("Document is empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.GetEnvelopeIdOfReceiver();
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
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]
|
|
||||||
[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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.GetReceiverMailOfReceiver();
|
|
||||||
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.GetEnvelopeIdOfReceiver();
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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 SignatureController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
|
||||||
/// </summary>
|
|
||||||
public SignatureController(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> GetAnnotsOfReceiver(string envelopeKey, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
int envelopeId = User.GetEnvelopeIdOfReceiver();
|
|
||||||
|
|
||||||
int receiverId = User.GetReceiverIdOfReceiver();
|
|
||||||
|
|
||||||
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
|
||||||
|
|
||||||
if (doc.Elements is not IEnumerable<SignatureDto> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides custom claim types for envelope-related information.
|
|
||||||
/// </summary>
|
|
||||||
public static class EnvelopeClaimTypes
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Claim type for the title of an envelope.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly string Title = $"Envelope{nameof(Title)}";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Claim type for the ID of an envelope.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly string Id = $"Envelope{nameof(Id)}";
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides helper methods for working with envelope-specific authentication claims.
|
|
||||||
/// </summary>
|
|
||||||
public static class ReceiverClaimExtensions
|
|
||||||
{
|
|
||||||
private static readonly string[] EnvelopeIdClaimTypes = [EnvelopeClaimTypes.Id, "envelope_id", "EnvelopeId"];
|
|
||||||
private static readonly string[] ReceiverIdClaimTypes = ["receiver_id", "ReceiverId"];
|
|
||||||
private static readonly string[] EnvelopeUuidClaimTypes = [ClaimTypes.NameIdentifier, "envelope_uuid", "EnvelopeUuid"];
|
|
||||||
private static readonly string[] ReceiverSignatureClaimTypes = [ClaimTypes.Hash, "receiver_sig", "ReceiverSignature"];
|
|
||||||
|
|
||||||
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, string claimType)
|
|
||||||
{
|
|
||||||
var value = user.FindFirstValue(claimType);
|
|
||||||
if (value is not null)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var identity = user.Identity;
|
|
||||||
var principalName = identity?.Name ?? "(anonymous)";
|
|
||||||
var authType = identity?.AuthenticationType ?? "(none)";
|
|
||||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
|
||||||
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetRequiredClaimOfReceiver(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 GetEnvelopeUuidOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeUuidClaimTypes);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver signature from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetReceiverSignatureOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ReceiverSignatureClaimTypes);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver display name from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetReceiverNameOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Name);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver email address from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetReceiverMailOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Email);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope title from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetEnvelopeTitleOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Title);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope identifier from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static int GetEnvelopeIdOfReceiver(this ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeIdClaimTypes);
|
|
||||||
if (!int.TryParse(envIdStr, out var envId))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Claim '{"envelope_id"}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return envId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public static int GetReceiverIdOfReceiver(this ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
var rcvIdStr = user.GetRequiredClaimOfReceiver(ReceiverIdClaimTypes);
|
|
||||||
if (!int.TryParse(rcvIdStr, out var rcvId))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Claim '{"receiver_id"}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return rcvId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The current HTTP context.</param>
|
|
||||||
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
|
|
||||||
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
|
|
||||||
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
|
|
||||||
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
|
|
||||||
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
|
|
||||||
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
|
|
||||||
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
|
|
||||||
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
|
|
||||||
new(ClaimTypes.Role, receiverRole)
|
|
||||||
};
|
|
||||||
|
|
||||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
var authProperties = new AuthenticationProperties
|
|
||||||
{
|
|
||||||
AllowRefresh = false,
|
|
||||||
IsPersistent = false
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.SignInAsync(
|
|
||||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
|
||||||
new ClaimsPrincipal(claimsIdentity),
|
|
||||||
authProperties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Models;
|
|
||||||
|
|
||||||
public class CustomImages : Dictionary<string, Image>
|
|
||||||
{
|
|
||||||
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Models;
|
|
||||||
|
|
||||||
public class MainViewModel
|
|
||||||
{
|
|
||||||
public string? Title { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
{
|
|
||||||
"ReverseProxy": {
|
|
||||||
"Routes": {
|
|
||||||
"receiver-ui-receiver": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/receiver/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-login": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/login/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-sender": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/sender/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-envelope": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/envelope/{EnvelopeKey}",
|
|
||||||
"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,36 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TCommand"></typeparam>
|
|
||||||
/// <typeparam name="TEntity"></typeparam>
|
|
||||||
public class CreateCommandHandler<TCommand, TEntity> : IRequestHandler<TCommand, TEntity>
|
|
||||||
where TCommand : class, IRequest<TEntity>
|
|
||||||
where TEntity : class
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
protected readonly IRepository<TEntity> Repository;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public CreateCommandHandler(IRepository<TEntity> repository)
|
|
||||||
{
|
|
||||||
Repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<TEntity> Handle(TCommand request, CancellationToken cancel) => Repository.CreateAsync(request, cancel);
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using MediatR;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TUpdateDto"></typeparam>
|
|
||||||
/// <typeparam name="TEntity"></typeparam>
|
|
||||||
public abstract record UpdateCommand<TUpdateDto, TEntity> : IRequest where TUpdateDto : class where TEntity : class
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public TUpdateDto Update { get; init; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public abstract Expression<Func<TEntity, bool>> BuildQueryExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TCommand"></typeparam>
|
|
||||||
/// <typeparam name="TUpdateDto"></typeparam>
|
|
||||||
/// <typeparam name="TEntity"></typeparam>
|
|
||||||
public class UpdateCommandHandler<TCommand, TUpdateDto, TEntity> : IRequestHandler<TCommand>
|
|
||||||
where TUpdateDto : class
|
|
||||||
where TEntity : class
|
|
||||||
where TCommand : UpdateCommand<TUpdateDto, TEntity>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
protected readonly IRepository<TEntity> Repository;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public UpdateCommandHandler(IRepository<TEntity> repository)
|
|
||||||
{
|
|
||||||
Repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task Handle(TCommand request, CancellationToken cancel)
|
|
||||||
=> Repository.UpdateAsync(request.Update, request.BuildQueryExpression(), cancel);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class DbTriggerParams : Dictionary<string, ICollection<string>>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record AnnotationCreateDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public long Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public int ElementId { get; init; } = -1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public string Name { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public string Value { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public string Type { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Horizontal position of the signature field on the page.
|
|
||||||
/// <br/><br/>
|
|
||||||
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, X increases to the right.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
|
||||||
/// Convert: <c>xDX = xInches * 100.0</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Conversion to PDF Points:</b> Multiply by 72 (PSPDFKit, iText7 use 1/72 inch).
|
|
||||||
/// Convert: <c>xPt = xInches * 72.0</c>
|
|
||||||
/// </summary>
|
|
||||||
public double? X { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vertical position of the signature field on the page.
|
|
||||||
/// <br/><br/>
|
|
||||||
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, Y increases downward.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
|
||||||
/// Convert: <c>yDX = yInches * 100.0</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Conversion to PDF Points (top-left origin):</b> Multiply by 72.
|
|
||||||
/// Convert: <c>yPt = yInches * 72.0</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Conversion to PDF Points (bottom-left origin - iText7):</b> Y-flip required.
|
|
||||||
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
|
||||||
/// </summary>
|
|
||||||
public double? Y { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public double? Width { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Not required for DevExpress")]
|
|
||||||
public double? Height { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Added to eliminate the need for SignatureDto in DevExpress
|
|
||||||
/// </summary>
|
|
||||||
public int? Page { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record AnnotationDto : AnnotationCreateDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public long Id { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public DateTime AddedWhen { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? ChangedWhen { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string? ChangedWho { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto
|
||||||
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EmailTemplateDto
|
public record EmailTemplateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -13,30 +16,16 @@ public record EmailTemplateDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; } = null!;
|
public required string Name { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime AddedWhen { get; set; }
|
public required string Body { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Body { get; set; }
|
public required string Subject { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der Betreff der E-Mail-Vorlage. Kann null sein.
|
|
||||||
/// </summary>
|
|
||||||
public string? Subject { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der Sprachcode der E-Mail-Vorlage.
|
|
||||||
/// </summary>
|
|
||||||
public string LangCode { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? ChangedWhen { get; set; }
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
using DigitalData.UserManager.Application.DTOs.User;
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
@@ -11,7 +10,7 @@ namespace EnvelopeGenerator.Application.Common.Dto;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeDto : IEnvelope
|
public record EnvelopeDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -75,13 +74,6 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EnvelopeTypeId { get; set; }
|
public int? EnvelopeTypeId { get; set; }
|
||||||
|
|
||||||
// TODO: use ReadAndConfirm property name
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use EnvelopeExtensions.IsReadAndConfirm extension metot instead.")]
|
|
||||||
public bool ReadOnly => this.IsReadAndConfirm();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -90,7 +82,7 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UseAccessCode { get; set; } = true;
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -28,14 +28,13 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>();
|
CreateMap<Envelope, EnvelopeDto>();
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<Domain.Entities.History, HistoryDto>();
|
||||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<Domain.Entities.History, HistoryCreateDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
|
||||||
|
|
||||||
// DTO to Entity mappings
|
// DTO to Entity mappings
|
||||||
CreateMap<ConfigDto, Config>();
|
CreateMap<ConfigDto, Config>();
|
||||||
@@ -44,15 +43,13 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||||
CreateMap<EnvelopeDto, Envelope>();
|
CreateMap<EnvelopeDto, Envelope>();
|
||||||
CreateMap<DocumentDto, Document>();
|
CreateMap<DocumentDto, Document>();
|
||||||
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryDto, Domain.Entities.History>();
|
||||||
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryCreateDto, Domain.Entities.History>();
|
||||||
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
||||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
|
||||||
.MapAddedWhen();
|
|
||||||
|
|
||||||
// Messaging mappings
|
// Messaging mappings
|
||||||
// for GTX messaging
|
// for GTX messaging
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
@@ -8,7 +6,7 @@ namespace EnvelopeGenerator.Application.Common.Dto;
|
|||||||
/// 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>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class SignatureDto : ISignature
|
public class SignatureDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the element.
|
/// Gets or sets the unique identifier of the element.
|
||||||
@@ -94,14 +92,4 @@ public class SignatureDto : ISignature
|
|||||||
/// Gets or sets the left position of the element (in layout terms).
|
/// Gets or sets the left position of the element (in layout terms).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Left => X;
|
public double Left => X;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<AnnotationDto>? Annotations { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for applying auditing timestamps during AutoMapper mappings.
|
|
||||||
/// </summary>
|
|
||||||
public static class AutoMapperAuditingExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Maps <see cref="IHasAddedWhen.AddedWhen"/> to the current UTC time.
|
|
||||||
/// </summary>
|
|
||||||
public static IMappingExpression<TSource, TDestination> MapAddedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
|
||||||
where TDestination : IHasAddedWhen
|
|
||||||
=> expression.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps <see cref="IHasChangedWhen.ChangedWhen"/> to the current UTC time.
|
|
||||||
/// </summary>
|
|
||||||
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
|
||||||
where TDestination : IHasChangedWhen
|
|
||||||
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class JsonExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj"></param>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ToJson(this object obj, JsonSerializerOptions? options = null)
|
|
||||||
=> JsonSerializer.Serialize(obj, options);
|
|
||||||
}
|
|
||||||
@@ -3,19 +3,8 @@ using System.Text;
|
|||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions
|
namespace EnvelopeGenerator.Application.Common.Extensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class LoggerExtensions
|
public static class LoggerExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger"></param>
|
|
||||||
/// <param name="envelopeReceiverId"></param>
|
|
||||||
/// <param name="exception"></param>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="args"></param>
|
|
||||||
public static void LogEnvelopeError(this ILogger logger, string envelopeReceiverId, Exception? exception = null, string? message = null, params object?[] args)
|
public static void LogEnvelopeError(this ILogger logger, string envelopeReceiverId, Exception? exception = null, string? message = null, params object?[] args)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder().AppendLine(envelopeReceiverId.DecodeEnvelopeReceiverId().ToTitle());
|
var sb = new StringBuilder().AppendLine(envelopeReceiverId.DecodeEnvelopeReceiverId().ToTitle());
|
||||||
@@ -29,15 +18,6 @@ namespace EnvelopeGenerator.Application.Common.Extensions
|
|||||||
logger.Log(LogLevel.Error, exception, sb.AppendLine(exception.Message).ToString(), args);
|
logger.Log(LogLevel.Error, exception, sb.AppendLine(exception.Message).ToString(), args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger"></param>
|
|
||||||
/// <param name="uuid"></param>
|
|
||||||
/// <param name="signature"></param>
|
|
||||||
/// <param name="exception"></param>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="args"></param>
|
|
||||||
public static void LogEnvelopeError(this ILogger logger, string? uuid, string? signature = null, Exception? exception = null, string? message = null, params object?[] args)
|
public static void LogEnvelopeError(this ILogger logger, string? uuid, string? signature = null, Exception? exception = null, string? message = null, params object?[] args)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder($"Envelope Uuid: {uuid}");
|
var sb = new StringBuilder($"Envelope Uuid: {uuid}");
|
||||||
@@ -54,11 +34,6 @@ namespace EnvelopeGenerator.Application.Common.Extensions
|
|||||||
logger.Log(LogLevel.Error, exception, sb.ToString(), args);
|
logger.Log(LogLevel.Error, exception, sb.ToString(), args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeReceiverTuple"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ToTitle(this (string? UUID, string? Signature) envelopeReceiverTuple)
|
public static string ToTitle(this (string? UUID, string? Signature) envelopeReceiverTuple)
|
||||||
{
|
{
|
||||||
return $"UUID is {envelopeReceiverTuple.UUID} and signature is {envelopeReceiverTuple.Signature}";
|
return $"UUID is {envelopeReceiverTuple.UUID} and signature is {envelopeReceiverTuple.Signature}";
|
||||||
|
|||||||
@@ -1,30 +1,14 @@
|
|||||||
using OtpNet;
|
using OtpNet;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
namespace EnvelopeGenerator.Application.Common.Extensions
|
||||||
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class StringExtension
|
public static class StringExtension
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="totp"></param>
|
|
||||||
/// <param name="secret"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool IsValidTotp(this string totp, string secret)
|
public static bool IsValidTotp(this string totp, string secret)
|
||||||
{
|
{
|
||||||
var secret_bytes = Base32Encoding.ToBytes(secret);
|
var secret_bytes = Base32Encoding.ToBytes(secret);
|
||||||
var secret_totp = new Totp(secret_bytes);
|
var secret_totp = new Totp(secret_bytes);
|
||||||
return secret_totp.VerifyTotp(totp, out _, VerificationWindow.RfcSpecifiedNetworkDelay);
|
return secret_totp.VerifyTotp(totp, out _, VerificationWindow.RfcSpecifiedNetworkDelay);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seperator"></param>
|
|
||||||
/// <param name="values"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Join(this IEnumerable<string> values, string seperator) => string.Join(seperator, values);
|
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ namespace EnvelopeGenerator.Application.Common.Extensions;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods for tasks
|
/// Extension methods for tasks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
|
||||||
public static class TaskExtensions
|
public static class TaskExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -18,7 +17,6 @@ public static class TaskExtensions
|
|||||||
/// <param name="factory">Exception provider</param>
|
/// <param name="factory">Exception provider</param>
|
||||||
/// <returns>The awaited result if not <c>null</c>.</returns>
|
/// <returns>The awaited result if not <c>null</c>.</returns>
|
||||||
/// <exception>Thrown if the result is <c>null</c>.</exception>
|
/// <exception>Thrown if the result is <c>null</c>.</exception>
|
||||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
|
||||||
public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception
|
public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception
|
||||||
{
|
{
|
||||||
var result = await task;
|
var result = await task;
|
||||||
@@ -35,7 +33,6 @@ public static class TaskExtensions
|
|||||||
/// <param name="factory">Exception provider</param>
|
/// <param name="factory">Exception provider</param>
|
||||||
/// <returns>The awaited collection if it is not <c>null</c> or empty.</returns>
|
/// <returns>The awaited collection if it is not <c>null</c> or empty.</returns>
|
||||||
/// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception>
|
/// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception>
|
||||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
|
||||||
public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception
|
public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception
|
||||||
{
|
{
|
||||||
var result = await task;
|
var result = await task;
|
||||||
@@ -50,33 +47,11 @@ public static class TaskExtensions
|
|||||||
/// <param name="task"></param>
|
/// <param name="task"></param>
|
||||||
/// <param name="act"></param>
|
/// <param name="act"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
|
||||||
public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act)
|
public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act)
|
||||||
{
|
{
|
||||||
var res = await task;
|
var res = await task;
|
||||||
return act(res);
|
return act(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="task"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
|
||||||
public static Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task) => task.Then(t => t.FirstOrDefault());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <typeparam name="TException"></typeparam>
|
|
||||||
/// <param name="task"></param>
|
|
||||||
/// <param name="factory"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Task<T> FirstAsync<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory)
|
|
||||||
where TException : Exception
|
|
||||||
=> task.Then(t => t.FirstOrDefault() ?? throw factory());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -93,13 +68,11 @@ public static class Exceptions
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
|
||||||
public static BadRequestException BadRequest() => new();
|
public static BadRequestException BadRequest() => new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
|
|
||||||
public static ForbiddenException Forbidden() => new();
|
public static ForbiddenException Forbidden() => new();
|
||||||
}
|
}
|
||||||
@@ -2,42 +2,16 @@
|
|||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
namespace EnvelopeGenerator.Application.Common.Extensions
|
||||||
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class XSSExtensions
|
public static class XSSExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <param name="encoder"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string? TryEncode(this string? value, UrlEncoder encoder) => value is null ? value : encoder.Encode(value);
|
public static string? TryEncode(this string? value, UrlEncoder encoder) => value is null ? value : encoder.Encode(value);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <param name="encoder"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string? TryEncode(this LocalizedString? value, UrlEncoder encoder) => value is null ? null : encoder.Encode(value);
|
public static string? TryEncode(this LocalizedString? value, UrlEncoder encoder) => value is null ? null : encoder.Encode(value);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="html"></param>
|
|
||||||
/// <param name="sanitizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string? TrySanitize(this string? html, HtmlSanitizer sanitizer) => html is null ? html : sanitizer.Sanitize(html);
|
public static string? TrySanitize(this string? html, HtmlSanitizer sanitizer) => html is null ? html : sanitizer.Sanitize(html);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="html"></param>
|
|
||||||
/// <param name="sanitizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string? TrySanitize(this LocalizedString? html, HtmlSanitizer sanitizer) => html is null ? null : sanitizer.Sanitize(html);
|
public static string? TrySanitize(this LocalizedString? html, HtmlSanitizer sanitizer) => html is null ? null : sanitizer.Sanitize(html);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,11 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System.Dynamic;
|
using System.Dynamic;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Instant"></param>
|
|
||||||
/// <param name="Structured"></param>
|
|
||||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -24,7 +15,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
public required ExpandoObject Annotations { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -36,7 +27,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string EmailAddress => Receiver?.EmailAddress
|
public string EmailAddress => Receiver?.EmailAddress
|
||||||
?? throw new InvalidOperationException($"Receiver is null." +
|
?? throw new InvalidOperationException($"Receiver is null." +
|
||||||
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
$"DocSignedNotification:\n{JsonConvert.SerializeObject(this, Format.Json.ForDiagnostics)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -48,41 +39,17 @@ public static class DocSignedNotificationExtensions
|
|||||||
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The DTO to convert.</param>
|
/// <param name="dto">The DTO to convert.</param>
|
||||||
/// <param name="psPdfKitAnnotation"></param>
|
/// <param name="annotations"></param>
|
||||||
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
||||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
|
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, ExpandoObject annotations)
|
||||||
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
|
=> new(dto) { Annotations = annotations };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Asynchronously converts a <see cref="Task{EnvelopeReceiverDto}"/> to a <see cref="DocSignedNotification"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dtoTask"></param>
|
/// <param name="dtoTask">The task that returns the DTO to convert.</param>
|
||||||
/// <param name="psPdfKitAnnotation"></param>
|
/// <param name="annotations"></param>
|
||||||
/// <returns></returns>
|
/// <returns>A task that represents the asynchronous conversion operation.</returns>
|
||||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
|
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, ExpandoObject annotations)
|
||||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { Annotations = annotations } : null;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="publisher"></param>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await publisher.Publish(notification, cancel);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await publisher.Publish(new RemoveSignatureNotification()
|
|
||||||
{
|
|
||||||
EnvelopeId = notification.EnvelopeId,
|
|
||||||
ReceiverId = notification.ReceiverId
|
|
||||||
}, cancel);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||||
|
|
||||||
@@ -9,18 +11,15 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly ISender _sender;
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
private readonly IRepository<ElementAnnotation> _repo;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="repository"></param>
|
/// <param name="sender"></param>
|
||||||
public AnnotationHandler(IRepository<ElementAnnotation> repository)
|
public AnnotationHandler(ISender sender)
|
||||||
{
|
{
|
||||||
_repo = repository;
|
_sender = sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -31,7 +30,11 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
|
await _sender.Send(new SaveDocStatusCommand()
|
||||||
await _repo.CreateAsync(annot.Structured, cancel);
|
{
|
||||||
|
Envelope = new() { Id = notification.EnvelopeId },
|
||||||
|
Receiver = new() { Id = notification.ReceiverId},
|
||||||
|
Value = JsonConvert.SerializeObject(notification.Annotations, Format.Json.ForAnnotations)
|
||||||
|
}, cancel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
|
||||||
{
|
|
||||||
private const string BlankAnnotationJson = "{}";
|
|
||||||
|
|
||||||
private readonly ISender _sender;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender"></param>
|
|
||||||
public DocStatusHandler(ISender sender)
|
|
||||||
{
|
|
||||||
_sender = sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
|
||||||
{
|
|
||||||
EnvelopeId = notification.EnvelopeId,
|
|
||||||
ReceiverId = notification.ReceiverId,
|
|
||||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
|
||||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
|
||||||
: BlankAnnotationJson
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
using EnvelopeGenerator.Application.Histories.Commands;
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||||
|
|
||||||
@@ -30,13 +31,15 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
|
|||||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if(notification.Receiver is null)
|
if(notification.Receiver is null)
|
||||||
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
if (notification.Receiver is null)
|
||||||
|
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {JsonConvert.SerializeObject(notification, Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
await _sender.Send(new CreateHistoryCommand()
|
await _sender.Send(new CreateHistoryCommand()
|
||||||
{
|
{
|
||||||
EnvelopeId = notification.EnvelopeId,
|
EnvelopeId = notification.EnvelopeId,
|
||||||
UserReference = notification.Receiver.EmailAddress,
|
UserReference = notification.Receiver.EmailAddress,
|
||||||
Status = EnvelopeStatus.DocumentSigned,
|
Status = EnvelopeStatus.DocumentSigned,
|
||||||
|
Comment = JsonConvert.SerializeObject(notification.Annotations, Format.Json.ForAnnotations)
|
||||||
}, cancel);
|
}, cancel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
|||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||||
|
|
||||||
@@ -46,25 +45,6 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
|||||||
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
|
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.Envelope.IsReadAndConfirm())
|
|
||||||
{
|
|
||||||
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
|
||||||
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
|
||||||
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
|
|
||||||
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
|
|
||||||
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
|
|
||||||
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
|
|
||||||
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
|
|
||||||
placeHolders["[FINAL_STATUS]"] = "Signatur";
|
|
||||||
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
|
|
||||||
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
|
|
||||||
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
|
|
||||||
}
|
|
||||||
|
|
||||||
return placeHolders;
|
return placeHolders;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class RemoveAnnotationHandler : INotificationHandler<RemoveSignatureNotification>
|
|
||||||
{
|
|
||||||
private readonly IRepository<ElementAnnotation> _repo;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public RemoveAnnotationHandler(IRepository<ElementAnnotation> repository)
|
|
||||||
{
|
|
||||||
_repo = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
notification.ThrowIfHasNoFilter();
|
|
||||||
return _repo.DeleteAsync(annots =>
|
|
||||||
{
|
|
||||||
// envelope ID filter
|
|
||||||
if (notification.EnvelopeId is int envelopeId)
|
|
||||||
annots = annots.Where(annot => annot.Element!.Document.EnvelopeId == envelopeId);
|
|
||||||
|
|
||||||
// envelope UUID filter
|
|
||||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
|
||||||
annots = annots.Where(annot => annot.Element!.Document.Envelope!.Uuid == envelopeUuid);
|
|
||||||
|
|
||||||
// receiver ID
|
|
||||||
if (notification.ReceiverId is int receiverId)
|
|
||||||
annots = annots.Where(annot => annot.Element!.ReceiverId == receiverId);
|
|
||||||
|
|
||||||
// receiver signature
|
|
||||||
if (notification.ReceiverSignature is string receiverSignature)
|
|
||||||
annots = annots.Where(annot => annot.Element!.Receiver!.Signature == receiverSignature);
|
|
||||||
|
|
||||||
return annots;
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using AngleSharp.Html;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class RemoveDocStatusHandler : INotificationHandler<RemoveSignatureNotification>
|
|
||||||
{
|
|
||||||
private readonly IRepository<Domain.Entities.DocumentStatus> _repo;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public RemoveDocStatusHandler(IRepository<Domain.Entities.DocumentStatus> repository)
|
|
||||||
{
|
|
||||||
_repo = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
notification.ThrowIfHasNoFilter();
|
|
||||||
return _repo.DeleteAsync(statuses =>
|
|
||||||
{
|
|
||||||
// envelope ID filter
|
|
||||||
if (notification.EnvelopeId is int envelopeId)
|
|
||||||
statuses = statuses.Where(status => status.EnvelopeId == envelopeId);
|
|
||||||
|
|
||||||
// envelope UUID filter
|
|
||||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
|
||||||
statuses = statuses.Where(status => status.Envelope!.Uuid == envelopeUuid);
|
|
||||||
|
|
||||||
// receiver Id filter
|
|
||||||
if (notification.ReceiverId is int receiverId)
|
|
||||||
statuses = statuses.Where(status => status.ReceiverId == receiverId);
|
|
||||||
|
|
||||||
// receiver signature filter
|
|
||||||
if (notification.ReceiverSignature is string receiverSignature)
|
|
||||||
statuses = statuses.Where(status => status.Receiver!.Signature == receiverSignature);
|
|
||||||
|
|
||||||
return statuses;
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class RemoveHistoryHandler : INotificationHandler<RemoveSignatureNotification>
|
|
||||||
{
|
|
||||||
private readonly IRepository<Domain.Entities.History> _repo;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public RemoveHistoryHandler(IRepository<Domain.Entities.History> repository)
|
|
||||||
{
|
|
||||||
_repo = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
notification.ThrowIfHasNoFilter();
|
|
||||||
return _repo.DeleteAsync(hists =>
|
|
||||||
{
|
|
||||||
hists = hists.Where(hist => hist.Status == EnvelopeStatus.DocumentSigned);
|
|
||||||
|
|
||||||
// envelope ID filter
|
|
||||||
if (notification.EnvelopeId is int envelopeId)
|
|
||||||
hists = hists.Where(hist => hist.EnvelopeId == envelopeId);
|
|
||||||
|
|
||||||
// envelope UUID filter
|
|
||||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
|
||||||
hists = hists.Where(hist => hist.Envelope!.Uuid == envelopeUuid);
|
|
||||||
|
|
||||||
// receiver ID filter
|
|
||||||
if (notification.ReceiverId is int receiverId)
|
|
||||||
hists = hists.Where(hist => hist.Receiver!.Id == receiverId);
|
|
||||||
|
|
||||||
// receiver signature filter
|
|
||||||
if (notification.ReceiverSignature is string receiverSignature)
|
|
||||||
hists = hists.Where(hist => hist.Receiver!.Signature == receiverSignature);
|
|
||||||
|
|
||||||
return hists;
|
|
||||||
}, cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="EnvelopeId"></param>
|
|
||||||
/// <param name="ReceiverId"></param>
|
|
||||||
/// <param name="EnvelopeUuid"></param>
|
|
||||||
/// <param name="ReceiverSignature"></param>
|
|
||||||
public record RemoveSignatureNotification(
|
|
||||||
int? EnvelopeId = null,
|
|
||||||
int? ReceiverId = null,
|
|
||||||
string? EnvelopeUuid = null,
|
|
||||||
string? ReceiverSignature = null
|
|
||||||
) : INotification
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public bool HasFilter =>
|
|
||||||
EnvelopeId is not null
|
|
||||||
|| ReceiverId is not null
|
|
||||||
|| EnvelopeUuid is not null
|
|
||||||
|| ReceiverSignature is not null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public void ThrowIfHasNoFilter()
|
|
||||||
{
|
|
||||||
if (!HasFilter)
|
|
||||||
throw new InvalidOperationException("At least one filter parameter must be provided.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
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.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -99,10 +98,10 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
|||||||
var placeHolders = CreatePlaceHolders(notification);
|
var placeHolders = CreatePlaceHolders(notification);
|
||||||
|
|
||||||
var temp = await TempRepo
|
var temp = await TempRepo
|
||||||
.Where(x => x.Name == notification.TemplateType.ToString())
|
.ReadOnly()
|
||||||
.SingleOrDefaultAsync(cancel)
|
.SingleOrDefaultAsync(x => x.Name == notification.TemplateType.ToString(), cancel)
|
||||||
?? throw new InvalidOperationException($"Receiver information is missing in the notification." +
|
?? throw new InvalidOperationException($"Receiver information is missing in the notification." +
|
||||||
$"{typeof(TNotification)}:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
$"{typeof(TNotification)}:\n {JsonConvert.SerializeObject(notification, Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, MailParams.Placeholders);
|
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, MailParams.Placeholders);
|
||||||
|
|
||||||
@@ -111,9 +110,9 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
|||||||
var emailOut = new EmailOut
|
var emailOut = new EmailOut
|
||||||
{
|
{
|
||||||
EmailAddress = notification.EmailAddress,
|
EmailAddress = notification.EmailAddress,
|
||||||
EmailBody = temp.Body,
|
EmailBody = TextToHtml(temp.Body),
|
||||||
EmailSubj = temp.Subject,
|
EmailSubj = temp.Subject,
|
||||||
AddedWhen = DateTime.Now,
|
AddedWhen = DateTime.UtcNow,
|
||||||
AddedWho = DispatcherParams.AddedWho,
|
AddedWho = DispatcherParams.AddedWho,
|
||||||
SendingProfile = DispatcherParams.SendingProfile,
|
SendingProfile = DispatcherParams.SendingProfile,
|
||||||
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
||||||
@@ -132,4 +131,22 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
|||||||
text = text.Replace(ph.Key, ph.Value);
|
text = text.Replace(ph.Key, ph.Value);
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string TextToHtml(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input)) return "";
|
||||||
|
|
||||||
|
// HTML encoding special characters
|
||||||
|
string encoded = System.Net.WebUtility.HtmlEncode(input);
|
||||||
|
|
||||||
|
// Convert tabs to (4 non-breaking spaces)
|
||||||
|
encoded = encoded.Replace("\t", " ");
|
||||||
|
|
||||||
|
// Convert line breaks to <br />
|
||||||
|
encoded = encoded.Replace("\r\n", "<br />"); // Windows
|
||||||
|
encoded = encoded.Replace("\r", "<br />"); // Mac old
|
||||||
|
encoded = encoded.Replace("\n", "<br />"); // Unix/Linux
|
||||||
|
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,14 +20,4 @@ public record ReceiverQueryBase
|
|||||||
/// Eindeutige Signatur des Empfängers
|
/// Eindeutige Signatur des Empfängers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual string? Signature { get; set; }
|
public virtual string? Signature { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether any of the specified query criteria have a value.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This property returns <c>true</c> if at least one of the fields
|
|
||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
|
||||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
|
||||||
/// </remarks>
|
|
||||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
|
||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.SQL;
|
namespace EnvelopeGenerator.Application.Common.SQL;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.SQL;
|
namespace EnvelopeGenerator.Application.Common.SQL;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using DigitalData.Core.Client;
|
using DigitalData.Core.Client;
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Interfaces.Services;
|
||||||
using EnvelopeGenerator.Application.Services;
|
using EnvelopeGenerator.Application.Services;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -46,6 +46,7 @@ public static class DependencyInjection
|
|||||||
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
||||||
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<DbTriggerParams>(config.GetSection(nameof(DbTriggerParams)));
|
||||||
|
|
||||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||||
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
||||||
|
|||||||
@@ -1,41 +1,12 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
namespace EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
using EnvelopeGenerator.Application.Common.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocStatus.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateDocStatusCommand : IRequest<DocumentStatus>
|
public record CreateDocStatusCommand : ModifyDocStatusCommandBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets timestamp when this record was added. Returns the StatusChangedWhen value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EnvelopeId { get; set; }
|
public DateTime AddedWhen => StatusChangedWhen;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int ReceiverId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the display value associated with the status.
|
|
||||||
/// </summary>
|
|
||||||
public string? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class CreateDocStatusCommandHandler : CreateCommandHandler<CreateDocStatusCommand, DocumentStatus>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public CreateDocStatusCommandHandler(IRepository<DocumentStatus> repository) : base(repository)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public record ModifyDocStatusCommandBase : EnvelopeReceiverQueryBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? EnvelopeId => Envelope.Id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ReceiverId => Receiver.Id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public override ReceiverQueryBase Receiver { get => base.Receiver; set => base.Receiver = value; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current status code.
|
||||||
|
/// </summary>
|
||||||
|
public DocumentStatus Status => Value is null ? DocumentStatus.Created : DocumentStatus.Signed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the display value associated with the status.
|
||||||
|
/// </summary>
|
||||||
|
public string? Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets timestamp when this record was added.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime StatusChangedWhen { get; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the current command to a new instance of the specified type.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TDest"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public TDest To<TDest>() where TDest : ModifyDocStatusCommandBase, new()
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Key = Key,
|
||||||
|
Envelope = Envelope,
|
||||||
|
Receiver = Receiver,
|
||||||
|
Value = Value
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a command to save the status of a document, either by creating a new status or updating an existing one based on the provided envelope and receiver identifiers.
|
||||||
|
/// It returns the identifier of the saved document status.
|
||||||
|
/// </summary>
|
||||||
|
public record SaveDocStatusCommand : ModifyDocStatusCommandBase, IRequest<DocumentStatusDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class SaveDocStatusCommandHandler : IRequestHandler<SaveDocStatusCommand, DocumentStatusDto?>
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
private readonly IRepository<DocumentStatus> _repo;
|
||||||
|
|
||||||
|
private readonly IRepository<Envelope> _envRepo;
|
||||||
|
|
||||||
|
private readonly IRepository<Receiver> _rcvRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
/// <param name="repo"></param>
|
||||||
|
/// <param name="rcvRepo"></param>
|
||||||
|
/// <param name="envRepo"></param>
|
||||||
|
public SaveDocStatusCommandHandler(IMapper mapper, IRepository<DocumentStatus> repo, IRepository<Receiver> rcvRepo, IRepository<Envelope> envRepo)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_repo = repo;
|
||||||
|
_rcvRepo = rcvRepo;
|
||||||
|
_envRepo = envRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<DocumentStatusDto?> Handle(SaveDocStatusCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
// ceck if exists
|
||||||
|
bool isExists = await _repo.ReadOnly().Where(request).AnyAsync(cancel);
|
||||||
|
|
||||||
|
var env = await _envRepo.ReadOnly().Where(request.Envelope).FirstAsync(cancel);
|
||||||
|
var rcv = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
|
||||||
|
|
||||||
|
request.Envelope.Id = env.Id;
|
||||||
|
request.Receiver.Id = rcv.Id;
|
||||||
|
|
||||||
|
if (isExists)
|
||||||
|
{
|
||||||
|
var uReq = request.To<UpdateDocStatusCommand>();
|
||||||
|
await _repo.UpdateAsync(uReq, q => q.Where(request), cancel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var cReq = request.To<CreateDocStatusCommand>();
|
||||||
|
await _repo.CreateAsync(cReq, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var docStatus = await _repo.ReadOnly().Where(request).SingleOrDefaultAsync(cancel);
|
||||||
|
|
||||||
|
return _mapper.Map<DocumentStatusDto>(docStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,14 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Commands;
|
using EnvelopeGenerator.Domain;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.DocStatus.Commands;
|
namespace EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Value"></param>
|
public record UpdateDocStatusCommand : ModifyDocStatusCommandBase
|
||||||
public record DocStatusUpdateDto(string? Value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record UpdateDocStatusCommand : UpdateCommand<DocStatusUpdateDto, DocumentStatus>
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets timestamp when this record was added. Returns the StatusChangedWhen value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EnvelopeId { get; set; }
|
public DateTime? ChangedWhen => StatusChangedWhen;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public int ReceiverId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the display value associated with the status.
|
|
||||||
/// </summary>
|
|
||||||
public string? Value { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public override Expression<Func<DocumentStatus, bool>> BuildQueryExpression()
|
|
||||||
{
|
|
||||||
return ds => ds.EnvelopeId == EnvelopeId && ds.ReceiverId == ReceiverId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
@@ -17,17 +16,10 @@ public class MappingProfile : Profile
|
|||||||
{
|
{
|
||||||
CreateMap<CreateDocStatusCommand, DocumentStatus>()
|
CreateMap<CreateDocStatusCommand, DocumentStatus>()
|
||||||
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
|
.ForMember(dest => dest.Receiver, opt => opt.Ignore());
|
||||||
.ForMember(dest => dest.Status, opt => opt.MapFrom(
|
|
||||||
src => src.Value == null
|
|
||||||
? Domain.Constants.DocumentStatus.Created
|
|
||||||
: Domain.Constants.DocumentStatus.Signed))
|
|
||||||
.MapAddedWhen();
|
|
||||||
|
|
||||||
CreateMap<UpdateDocStatusCommand, DocumentStatus>()
|
CreateMap<UpdateDocStatusCommand, DocumentStatus>()
|
||||||
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
|
.ForMember(dest => dest.Receiver, opt => opt.Ignore());
|
||||||
.ForMember(dest => dest.StatusChangedWhen, opt => opt.MapFrom(src => DateTime.Now))
|
|
||||||
.MapChangedWhen();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ using EnvelopeGenerator.Domain.Entities;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Documents.Queries;
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
@@ -13,14 +12,14 @@ 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>
|
||||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
|
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto?>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles queries for reading <see cref="Document"/> data based on either the document ID or the envelope ID.
|
/// Handles queries for reading <see cref="Document"/> data based on either the document ID or the envelope ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, DocumentDto>
|
public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, DocumentDto?>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TempRepo for accessing <see cref="Document"/> entities.
|
/// TempRepo for accessing <see cref="Document"/> entities.
|
||||||
@@ -51,22 +50,20 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
|||||||
/// <exception cref="InvalidOperationException">
|
/// <exception cref="InvalidOperationException">
|
||||||
/// Thrown when neither <see cref="ReadDocumentQuery.Id"/> nor <see cref="ReadDocumentQuery.EnvelopeId"/> is provided.
|
/// Thrown when neither <see cref="ReadDocumentQuery.Id"/> nor <see cref="ReadDocumentQuery.EnvelopeId"/> is provided.
|
||||||
/// </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);
|
|
||||||
|
|
||||||
if (query.Id is not null)
|
if (query.Id is not null)
|
||||||
{
|
{
|
||||||
var doc = await docQuery.Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
|
var doc = await _repo.ReadOnly().Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
|
||||||
|
|
||||||
return _mapper.Map<DocumentDto>(doc);
|
return _mapper.Map<DocumentDto>(doc);
|
||||||
}
|
}
|
||||||
else if (query.EnvelopeId is not null)
|
else if (query.EnvelopeId is not null)
|
||||||
{
|
{
|
||||||
var doc = await docQuery.Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
|
var doc = await _repo.ReadOnly().Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
|
||||||
return _mapper.Map<DocumentDto>(doc);
|
return _mapper.Map<DocumentDto>(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotFoundException();
|
throw new InvalidOperationException(
|
||||||
|
$"Invalid {nameof(ReadDocumentQuery)}: either {nameof(query.Id)} or {nameof(query.EnvelopeId)} must be provided.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using EnvelopeGenerator.Domain;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ein Befehl zum Zurücksetzen einer E-Mail-Vorlage auf die Standardwerte.
|
||||||
|
/// Erbt von <see cref="EmailTemplateQuery"/> und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage.<br/><br/>
|
||||||
|
/// Beispiele:<br/>
|
||||||
|
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
|
||||||
|
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
|
||||||
|
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
|
||||||
|
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
|
||||||
|
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
|
||||||
|
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
|
||||||
|
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
|
||||||
|
/// 7 - DocumentRejected_ADM (Für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
|
||||||
|
/// 8 - DocumentRejected_REC (Für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
|
||||||
|
/// 9 - DocumentRejected_REC_2 (Für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.<br/>
|
||||||
|
/// </summary>
|
||||||
|
public record ResetEmailTemplateCommand : EmailTemplateQuery, IRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orginal"></param>
|
||||||
|
public ResetEmailTemplateCommand(EmailTemplateQuery? orginal = null) : base(orginal ?? new())
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">Die optionale ID der E-Mail-Vorlage, die zurückgesetzt werden soll.</param>
|
||||||
|
/// <param name="Type">Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional).</param>
|
||||||
|
public ResetEmailTemplateCommand(int? Id = null, EmailTemplateType? Type = null) : base(Id, Type)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,38 +1,11 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EmailTemplates.Commands;
|
namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ein Befehl zum Zurücksetzen einer E-Mail-Vorlage auf die Standardwerte.
|
|
||||||
/// Erbt von <see cref="IEmailTemplateQuery"/> und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage.<br/><br/>
|
|
||||||
/// </summary>
|
|
||||||
public record ResetEmailTemplateCommand : IEmailTemplateQuery, IRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
|
|
||||||
/// </summary>
|
|
||||||
public int? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
|
|
||||||
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
|
|
||||||
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
|
|
||||||
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
|
|
||||||
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
|
|
||||||
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
|
|
||||||
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
|
|
||||||
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
|
|
||||||
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
|
|
||||||
/// </summary>
|
|
||||||
public EmailTemplateType? Type { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using MediatR;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Update;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Befehl zum Aktualisieren einer E-Mail-Vorlage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Body">
|
||||||
|
/// (Optional)Der neue Inhalt des E-Mail-Textkörpers. Wenn null, bleibt der vorhandene Inhalt unverändert.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Subject">
|
||||||
|
/// (Optional) Der neue Betreff der E-Mail. Wenn null, bleibt der vorhandene Betreff unverändert.
|
||||||
|
/// </param>
|
||||||
|
public record UpdateEmailTemplateCommand(string? Body = null, string? Subject = null) : IRequest
|
||||||
|
{
|
||||||
|
/// <param>
|
||||||
|
/// Die Abfrage, die die E-Mail-Vorlage darstellt, die aktualisiert werden soll.
|
||||||
|
/// </param>
|
||||||
|
[JsonIgnore]
|
||||||
|
public EmailTemplateQuery? EmailTemplateQuery { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTime ChangedWhen { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Update;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateEmailTemplateCommandHandler : IRequestHandler<UpdateEmailTemplateCommand>
|
||||||
|
{
|
||||||
|
private readonly IRepository<EmailTemplate> _repository;
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
public UpdateEmailTemplateCommandHandler(IRepository<EmailTemplate> repository, IMapper mapper)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
/// <exception cref="NotFoundException"></exception>
|
||||||
|
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
|
||||||
|
public async Task Handle(UpdateEmailTemplateCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
EmailTemplateDto? tempDto;
|
||||||
|
|
||||||
|
if (request.EmailTemplateQuery?.Id is int id)
|
||||||
|
{
|
||||||
|
var temp = await _repository.ReadOnly().Where(t => t.Id == id).FirstOrDefaultAsync(cancel);
|
||||||
|
tempDto = _mapper.Map<EmailTemplateDto>(temp);
|
||||||
|
}
|
||||||
|
else if (request!.EmailTemplateQuery!.Type is EmailTemplateType type)
|
||||||
|
{
|
||||||
|
var temp = await _repository.ReadOnly().Where(t => t.Name == type.ToString()).FirstOrDefaultAsync(cancel);
|
||||||
|
tempDto = _mapper.Map<EmailTemplateDto>(temp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Both id and type is null. Id: " + request.EmailTemplateQuery.Id +". Type: " + request.EmailTemplateQuery.Type.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempDto == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Body is not null)
|
||||||
|
tempDto.Body = request.Body;
|
||||||
|
|
||||||
|
if (request.Subject is not null)
|
||||||
|
tempDto.Subject = request.Subject;
|
||||||
|
|
||||||
|
await _repository.UpdateAsync(tempDto, t => t.Id == tempDto.Id, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Application.Common.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EmailTemplates.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Body"></param>
|
|
||||||
/// <param name="Subject"></param>
|
|
||||||
public record EmailTemplateUpdateDto(string Body, string Subject);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Befehl zum Aktualisieren einer E-Mail-Vorlage.
|
|
||||||
/// </summary>
|
|
||||||
public record UpdateEmailTemplateCommand : UpdateCommand<EmailTemplateUpdateDto, EmailTemplate>, IEmailTemplateQuery
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
|
|
||||||
/// </summary>
|
|
||||||
public int? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
|
|
||||||
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
|
|
||||||
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
|
|
||||||
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
|
|
||||||
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
|
|
||||||
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
|
|
||||||
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
|
|
||||||
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
|
|
||||||
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
|
|
||||||
/// </summary>
|
|
||||||
public EmailTemplateType? Type { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public override Expression<Func<EmailTemplate, bool>> BuildQueryExpression()
|
|
||||||
=> Id is int id
|
|
||||||
? temp => temp.Id == id
|
|
||||||
: temp => temp!.Name == Type.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class UpdateEmailTemplateCommandHandler : UpdateCommandHandler<UpdateEmailTemplateCommand, EmailTemplateUpdateDto, EmailTemplate>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
public UpdateEmailTemplateCommandHandler(IRepository<EmailTemplate> repository) : base(repository)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EmailTemplates;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repräsentiert eine Abfrage für E-Mail-Vorlagen, die für Absender und Empfänger von Umschlägen verwendet werden.
|
||||||
|
/// Die Standardkultur ist "de-DE".
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">Die eindeutige Kennung der E-Mail-Vorlage (optional).</param>
|
||||||
|
/// <param name="Type">Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:
|
||||||
|
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.
|
||||||
|
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.
|
||||||
|
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.
|
||||||
|
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.
|
||||||
|
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.
|
||||||
|
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.
|
||||||
|
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.
|
||||||
|
/// 7 - DocumentRejected_ADM (Für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.
|
||||||
|
/// 8 - DocumentRejected_REC (Für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.
|
||||||
|
/// 9 - DocumentRejected_REC_2 (Für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
|
||||||
|
/// </param>
|
||||||
|
public record EmailTemplateQuery(int? Id = null, EmailTemplateType? Type = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EmailTemplates;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stellt eine Schnittstelle für Abfragen von E-Mail-Vorlagen dar, die für Absender und Empfänger von Umschlägen verwendet werden.
|
|
||||||
/// Die Standardkultur ist "de-DE".
|
|
||||||
/// </summary>
|
|
||||||
public interface IEmailTemplateQuery
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
|
|
||||||
/// </summary>
|
|
||||||
int? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
|
|
||||||
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
|
|
||||||
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
|
|
||||||
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
|
|
||||||
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
|
|
||||||
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
|
|
||||||
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
|
|
||||||
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
|
|
||||||
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
|
|
||||||
/// </summary>
|
|
||||||
EmailTemplateType? Type { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.EmailTemplates.Commands;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EmailTemplates;
|
namespace EnvelopeGenerator.Application.EmailTemplates;
|
||||||
|
|
||||||
@@ -16,9 +19,6 @@ public class MappingProfile : Profile
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, ReadEmailTemplateResponse>();
|
||||||
|
|
||||||
CreateMap<EmailTemplateUpdateDto, EmailTemplate>()
|
|
||||||
.MapChangedWhen();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stellt eine Abfrage dar, um eine E-Mail-Vorlage zu lesen.
|
||||||
|
/// Diese Klasse erbt von <see cref="EmailTemplateQuery"/>.
|
||||||
|
/// </summary>
|
||||||
|
public record ReadEmailTemplateQuery : EmailTemplateQuery, IRequest<ReadEmailTemplateResponse?>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.Interfaces.Repositories;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class ReadEmailTemplateQueryHandler : IRequestHandler<ReadEmailTemplateQuery, ReadEmailTemplateResponse?>
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
|
||||||
|
private readonly IEmailTemplateRepository _repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mapper">
|
||||||
|
/// <param name="repository">
|
||||||
|
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
|
||||||
|
/// </param>
|
||||||
|
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
|
||||||
|
public ReadEmailTemplateQueryHandler(IMapper mapper, IEmailTemplateRepository repository)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
[Obsolete("Use IRepository")]
|
||||||
|
public async Task<ReadEmailTemplateResponse?> Handle(ReadEmailTemplateQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var temp = request.Id is int id
|
||||||
|
? await _repository.ReadByIdAsync(id)
|
||||||
|
: request.Type is EmailTemplateType type
|
||||||
|
? await _repository.ReadByNameAsync(type)
|
||||||
|
: throw new InvalidOperationException("Either a valid integer ID or a valid EmailTemplateType must be provided in the request.");
|
||||||
|
|
||||||
|
var res = _mapper.Map<ReadEmailTemplateResponse>(temp);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stellt die Antwort für eine Abfrage von E-Mail-Vorlagen bereit.
|
||||||
|
/// </summary>
|
||||||
|
public class ReadEmailTemplateResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Die eindeutige Kennung der E-Mail-Vorlage.
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name des Typs
|
||||||
|
/// </summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime AddedWhen { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein.
|
||||||
|
/// </summary>
|
||||||
|
public string? Body { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Der Betreff der E-Mail-Vorlage. Kann null sein.
|
||||||
|
/// </summary>
|
||||||
|
public string? Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ChangedWhen { get; set; }
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
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 DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EmailTemplates.Queries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stellt eine Abfrage dar, um eine E-Mail-Vorlage zu lesen.
|
|
||||||
/// Diese Klasse erbt von <see cref="IEmailTemplateQuery"/>.
|
|
||||||
/// </summary>
|
|
||||||
public record ReadEmailTemplateQuery : IEmailTemplateQuery, IRequest<IEnumerable<EmailTemplateDto>>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
|
|
||||||
/// </summary>
|
|
||||||
public int? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
|
|
||||||
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
|
|
||||||
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
|
|
||||||
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
|
|
||||||
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
|
|
||||||
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
|
|
||||||
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
|
|
||||||
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
|
|
||||||
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
|
|
||||||
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
|
|
||||||
/// </summary>
|
|
||||||
public EmailTemplateType? Type { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string? LangCode { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class ReadEmailTemplateQueryExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender"></param>
|
|
||||||
/// <param name="id"></param>
|
|
||||||
/// <param name="langCode"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<EmailTemplateDto?> ReadEmailTemplateAsync(this ISender sender, int id, string langCode, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var result = await sender.Send(new ReadEmailTemplateQuery { Id = id, LangCode = langCode }, cancel);
|
|
||||||
return result.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender"></param>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <param name="langCode"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<EmailTemplateDto?> ReadEmailTemplateAsync(this ISender sender, EmailTemplateType type, string langCode, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var result = await sender.Send(new ReadEmailTemplateQuery { Type = type, LangCode = langCode }, cancel);
|
|
||||||
return result.FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ReadEmailTemplateQueryHandler : IRequestHandler<ReadEmailTemplateQuery, IEnumerable<EmailTemplateDto>>
|
|
||||||
{
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
private readonly IRepository<EmailTemplate> _repo;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mapper">
|
|
||||||
/// <param name="repo">
|
|
||||||
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
|
|
||||||
/// </param>
|
|
||||||
public ReadEmailTemplateQueryHandler(IMapper mapper, IRepository<EmailTemplate> repo)
|
|
||||||
{
|
|
||||||
_mapper = mapper;
|
|
||||||
_repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public async Task<IEnumerable<EmailTemplateDto>> Handle(ReadEmailTemplateQuery request, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var query = _repo.Query;
|
|
||||||
|
|
||||||
if (request.Id is int id)
|
|
||||||
query = query.Where(temp => temp.Id == id);
|
|
||||||
|
|
||||||
if (request.Type is EmailTemplateType type)
|
|
||||||
query = query.Where(temp => temp.Name == type.ToString());
|
|
||||||
|
|
||||||
if (request.LangCode is string langCode)
|
|
||||||
query = query.Where(temp => temp.LangCode == langCode);
|
|
||||||
|
|
||||||
var entity = await query.ToListAsync(cancel);
|
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<EmailTemplateDto>>(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,21 +14,17 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
|
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.2.1" />
|
||||||
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
||||||
<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="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
|
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
<PackageReference Include="System.Formats.Asn1" Version="10.0.3" />
|
|
||||||
<PackageReference Include="System.Security.AccessControl" Version="6.0.1" />
|
|
||||||
<PackageReference Include="UserManager" Version="1.1.3" />
|
<PackageReference Include="UserManager" Version="1.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -37,7 +33,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
@@ -67,9 +63,6 @@
|
|||||||
<LastGenOutput>Model.Designer.cs</LastGenOutput>
|
<LastGenOutput>Model.Designer.cs</LastGenOutput>
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Resources\Resource.fr-FR.resx">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
|
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
@@ -80,7 +73,7 @@
|
|||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.3" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>7.0.5</Version>
|
<Version>7.0.5</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -88,6 +81,7 @@
|
|||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -95,6 +89,7 @@
|
|||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
@@ -47,13 +47,7 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|||||||
/// Die Antwort enthält Details wie den Include, die Zuordnung zwischen Umschlag und Empfänger
|
/// Die Antwort enthält Details wie den Include, die Zuordnung zwischen Umschlag und Empfänger
|
||||||
/// sowie zusätzliche Metadaten.
|
/// sowie zusätzliche Metadaten.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>
|
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Optionaler Benutzernamefilter, um Ergebnisse auf Umschläge eines bestimmten Besitzers einzuschränken.
|
|
||||||
/// </summary>
|
|
||||||
public string? Username { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -67,11 +61,10 @@ public static class Extensions
|
|||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="cancel"></param>
|
/// <param name="cancel"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string key, CancellationToken cancel = default)
|
public static Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string key, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var q = new ReadEnvelopeReceiverQuery() { Key = key };
|
var q = new ReadEnvelopeReceiverQuery() { Key = key };
|
||||||
var envRcvs = await mediator.Send(q, cancel);
|
return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
|
||||||
return envRcvs.FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -82,34 +75,33 @@ public static class Extensions
|
|||||||
/// <param name="signature"></param>
|
/// <param name="signature"></param>
|
||||||
/// <param name="cancel"></param>
|
/// <param name="cancel"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string uuid, string signature, CancellationToken cancel = default)
|
public static Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string uuid, string signature, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var q = new ReadEnvelopeReceiverQuery();
|
var q = new ReadEnvelopeReceiverQuery();
|
||||||
q.Envelope.Uuid = uuid;
|
q.Envelope.Uuid = uuid;
|
||||||
q.Receiver.Signature = signature;
|
q.Receiver.Signature = signature;
|
||||||
var envRcvs = await mediator.Send(q, cancel);
|
return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
|
||||||
return envRcvs.FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verarbeitet <see cref="ReadEnvelopeReceiverQuery"/> und liefert passende <see cref="EnvelopeReceiverDto"/>-Ergebnisse.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
|
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
|
||||||
{
|
{
|
||||||
private readonly IRepository<EnvelopeReceiver> _repo;
|
private readonly IRepository<EnvelopeReceiver> _repo;
|
||||||
private readonly IRepository<Receiver> _rcvRepo;
|
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="envelopeReceiver"></param>
|
/// <param name="envelopeReceiver"></param>
|
||||||
/// <param name="rcvRepo"></param>
|
|
||||||
/// <param name="mapper"></param>
|
/// <param name="mapper"></param>
|
||||||
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
|
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IMapper mapper)
|
||||||
{
|
{
|
||||||
_repo = envelopeReceiver;
|
_repo = envelopeReceiver;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_rcvRepo = rcvRepo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -121,10 +113,7 @@ public static class Extensions
|
|||||||
/// <exception cref="BadRequestException"></exception>
|
/// <exception cref="BadRequestException"></exception>
|
||||||
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
|
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var q = _repo.Query.Where(request, notnull: false);
|
var q = _repo.ReadOnly().Where(request, notnull: false);
|
||||||
|
|
||||||
if (request.Username is string username)
|
|
||||||
q = q.Where(er => er.Envelope!.User.Username == username);
|
|
||||||
|
|
||||||
if (request.Envelope.Status is not null)
|
if (request.Envelope.Status is not null)
|
||||||
{
|
{
|
||||||
@@ -149,15 +138,6 @@ public static class Extensions
|
|||||||
.Include(er => er.Receiver)
|
.Include(er => er.Receiver)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0)
|
|
||||||
{
|
|
||||||
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel);
|
|
||||||
|
|
||||||
foreach (var envRcv in envRcvs)
|
|
||||||
envRcv.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
|
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
|
||||||
using EnvelopeGenerator.Application.Receivers.Queries;
|
|
||||||
using MediatR;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a query for reading an envelope receiver including sensitive fields
|
|
||||||
/// (access code, phone number) that are excluded from the standard <see cref="ReadEnvelopeReceiverQuery"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Returns a single <see cref="EnvelopeReceiverSecretDto"/> matched by UUID and receiver signature.
|
|
||||||
/// Equivalent to the legacy <c>ReadWithSecretByUuidSignatureAsync</c> service method.
|
|
||||||
/// </remarks>
|
|
||||||
public record ReadEnvelopeReceiverSecretQuery
|
|
||||||
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
|
|
||||||
IRequest<EnvelopeReceiverSecretDto?>;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static class ReadEnvelopeReceiverSecretQueryExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a <see cref="ReadEnvelopeReceiverSecretQuery"/> using the composite key (uuid::signature).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mediator">The mediator instance.</param>
|
|
||||||
/// <param name="key">Composite key in the format <c>uuid::signature</c>.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
/// <returns>The matching <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if not found.</returns>
|
|
||||||
public static Task<EnvelopeReceiverSecretDto?> ReadEnvelopeReceiverSecretAsync(
|
|
||||||
this IMediator mediator,
|
|
||||||
string key,
|
|
||||||
CancellationToken cancel = default)
|
|
||||||
=> mediator.Send(new ReadEnvelopeReceiverSecretQuery { Key = key }, cancel);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a <see cref="ReadEnvelopeReceiverSecretQuery"/> using UUID and receiver signature.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mediator">The mediator instance.</param>
|
|
||||||
/// <param name="uuid">Envelope UUID.</param>
|
|
||||||
/// <param name="signature">Receiver signature.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
/// <returns>The matching <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if not found.</returns>
|
|
||||||
public static Task<EnvelopeReceiverSecretDto?> ReadEnvelopeReceiverSecretAsync(
|
|
||||||
this IMediator mediator,
|
|
||||||
string uuid,
|
|
||||||
string signature,
|
|
||||||
CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var q = new ReadEnvelopeReceiverSecretQuery();
|
|
||||||
q.Envelope.Uuid = uuid;
|
|
||||||
q.Receiver.Signature = signature;
|
|
||||||
return mediator.Send(q, cancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles <see cref="ReadEnvelopeReceiverSecretQuery"/> and returns a
|
|
||||||
/// <see cref="EnvelopeReceiverSecretDto"/> containing sensitive fields.
|
|
||||||
/// </summary>
|
|
||||||
public class ReadEnvelopeReceiverSecretQueryHandler
|
|
||||||
: IRequestHandler<ReadEnvelopeReceiverSecretQuery, EnvelopeReceiverSecretDto?>
|
|
||||||
{
|
|
||||||
private readonly IRepository<EnvelopeReceiver> _repo;
|
|
||||||
private readonly IRepository<Receiver> _rcvRepo;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="ReadEnvelopeReceiverSecretQueryHandler"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeReceiver">Repository for <see cref="EnvelopeReceiver"/>.</param>
|
|
||||||
/// <param name="rcvRepo">Repository for <see cref="Receiver"/>.</param>
|
|
||||||
/// <param name="mapper">AutoMapper instance.</param>
|
|
||||||
public ReadEnvelopeReceiverSecretQueryHandler(
|
|
||||||
IRepository<EnvelopeReceiver> envelopeReceiver,
|
|
||||||
IRepository<Receiver> rcvRepo,
|
|
||||||
IMapper mapper)
|
|
||||||
{
|
|
||||||
_repo = envelopeReceiver;
|
|
||||||
_rcvRepo = rcvRepo;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles the query and returns the matching <see cref="EnvelopeReceiverSecretDto"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The query containing filter criteria.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The matched <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if no record is found.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<EnvelopeReceiverSecretDto?> Handle(
|
|
||||||
ReadEnvelopeReceiverSecretQuery request,
|
|
||||||
CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var q = _repo.Query.Where(request, notnull: false);
|
|
||||||
|
|
||||||
var envRcvs = await q
|
|
||||||
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
|
|
||||||
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
|
|
||||||
.Include(er => er.Envelope).ThenInclude(e => e!.User)
|
|
||||||
.Include(er => er.Receiver)
|
|
||||||
.ToListAsync(cancel);
|
|
||||||
|
|
||||||
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0)
|
|
||||||
{
|
|
||||||
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel);
|
|
||||||
|
|
||||||
foreach (var item in envRcvs)
|
|
||||||
item.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
var envRcv = envRcvs.FirstOrDefault();
|
|
||||||
if (envRcv is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return _mapper.Map<EnvelopeReceiverSecretDto>(envRcv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -68,6 +68,6 @@ public class ReceiverAlreadySignedQueryHandler : IRequestHandler<ReceiverAlready
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<bool> Handle(ReceiverAlreadySignedQuery request, CancellationToken cancel = default)
|
public async Task<bool> Handle(ReceiverAlreadySignedQuery request, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
return await _repo.Query.Where(request).Where(h => h.Status == EnvelopeStatus.DocumentSigned).AnyAsync(cancel);
|
return await _repo.ReadOnly().Where(request).Where(h => h.Status == EnvelopeStatus.DocumentSigned).AnyAsync(cancel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using MediatR;
|
|
||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.EnvelopeTypes.Queries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
|
||||||
{
|
|
||||||
private readonly IRepository<EnvelopeType> _repository;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
|
|
||||||
{
|
|
||||||
_repository = repository;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var types = await _repository.Query.ToListAsync(cancellationToken);
|
|
||||||
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
@@ -33,18 +33,7 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID des Absenders
|
/// ID des Absenders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal int UserId { get; private set; }
|
public int UserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public CreateEnvelopeCommand WithAuth(int userId)
|
|
||||||
{
|
|
||||||
UserId = userId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines which component is used for envelope processing.
|
/// Determines which component is used for envelope processing.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,18 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repräsentiert eine Abfrage für Umschläge.
|
/// Repräsentiert eine Abfrage für Umschläge.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage des Include des Umschlags
|
/// Abfrage des Include des Umschlags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EnvelopeStatusQuery? Status { get; init; }
|
public EnvelopeStatusQuery? Status { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
|
||||||
/// </summary>
|
|
||||||
public int? UserId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setzt den Benutzerkontext für die Abfrage.
|
|
||||||
/// </summary>
|
|
||||||
public ReadEnvelopeQuery Authorize(int userId) => this with { UserId = userId };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -81,61 +66,3 @@ public record EnvelopeStatusQuery
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public EnvelopeStatus[]? Ignore { get; init; }
|
public EnvelopeStatus[]? Ignore { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verarbeitet <see cref="ReadEnvelopeQuery"/> und liefert passende <see cref="EnvelopeDto"/>-Ergebnisse.
|
|
||||||
/// </summary>
|
|
||||||
public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnumerable<EnvelopeDto>>
|
|
||||||
{
|
|
||||||
private readonly IRepository<Envelope> _repository;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public ReadEnvelopeQueryHandler(IRepository<Envelope> repository, IMapper mapper)
|
|
||||||
{
|
|
||||||
_repository = repository;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<IEnumerable<EnvelopeDto>> Handle(ReadEnvelopeQuery request, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var query = _repository.Query;
|
|
||||||
|
|
||||||
if (request.UserId is int userId)
|
|
||||||
query = query.Where(e => e.UserId == userId);
|
|
||||||
|
|
||||||
if (request.Id is int id)
|
|
||||||
query = query.Where(e => e.Id == id);
|
|
||||||
|
|
||||||
if (request.Uuid is string uuid)
|
|
||||||
query = query.Where(e => e.Uuid == uuid);
|
|
||||||
|
|
||||||
if (request.Status is { } status)
|
|
||||||
{
|
|
||||||
if (status.Min is not null)
|
|
||||||
query = query.Where(e => e.Status >= status.Min);
|
|
||||||
if (status.Max is not null)
|
|
||||||
query = query.Where(e => e.Status <= status.Max);
|
|
||||||
if (status.Include?.Length > 0)
|
|
||||||
query = query.Where(e => status.Include.Contains(e.Status));
|
|
||||||
if (status.Ignore?.Length > 0)
|
|
||||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
|
||||||
}
|
|
||||||
|
|
||||||
var envelopes = await query
|
|
||||||
.Include(e => e.Documents)
|
|
||||||
.ToListAsync(cancel);
|
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
|
||||||
using MediatR;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
|
||||||
@@ -10,54 +6,6 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// Eine Abfrage, um die zuletzt verwendete Anrede eines Empfängers zu ermitteln,
|
/// Eine Abfrage, um die zuletzt verwendete Anrede eines Empfängers zu ermitteln,
|
||||||
/// damit diese für zukünftige Umschläge wiederverwendet werden kann.
|
/// damit diese für zukünftige Umschläge wiederverwendet werden kann.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadReceiverNameQuery() : ReceiverQueryBase, IRequest<string?>;
|
public record ReadReceiverNameQuery() : ReadReceiverQuery
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ReadReceiverNameQueryHandler : IRequestHandler<ReadReceiverNameQuery, string?>
|
|
||||||
{
|
{
|
||||||
private readonly IRepository<EnvelopeReceiver> _envelopeReceiverRepository;
|
|
||||||
private readonly IRepository<Receiver> _receiverRepository;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeReceiverRepository"></param>
|
|
||||||
/// <param name="receiverRepository"></param>
|
|
||||||
public ReadReceiverNameQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiverRepository, IRepository<Receiver> receiverRepository)
|
|
||||||
{
|
|
||||||
_envelopeReceiverRepository = envelopeReceiverRepository;
|
|
||||||
_receiverRepository = receiverRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<string?> Handle(ReadReceiverNameQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var receiverQuery = _receiverRepository.Query.AsNoTracking();
|
|
||||||
|
|
||||||
if (request.Id is int id)
|
|
||||||
receiverQuery = receiverQuery.Where(r => r.Id == id);
|
|
||||||
if (request.EmailAddress is string email)
|
|
||||||
receiverQuery = receiverQuery.Where(r => r.EmailAddress == email);
|
|
||||||
if (request.Signature is string signature)
|
|
||||||
receiverQuery = receiverQuery.Where(r => r.Signature == signature);
|
|
||||||
|
|
||||||
var receiver = await receiverQuery.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
if (receiver is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var erName = await _envelopeReceiverRepository.Query
|
|
||||||
.Where(er => er.ReceiverId == receiver.Id)
|
|
||||||
.OrderByDescending(er => er.AddedWhen)
|
|
||||||
.Select(er => er.Name)
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
|
|
||||||
return erName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ public class CreateHistoryCommandHandler : IRequestHandler<CreateHistoryCommand,
|
|||||||
if(request.UserReference is null)
|
if(request.UserReference is null)
|
||||||
{
|
{
|
||||||
var receivers = await _erRepo
|
var receivers = await _erRepo
|
||||||
.Query
|
.ReadOnly()
|
||||||
.Where(request)
|
.Where(request)
|
||||||
.Include(er => er.Receiver)
|
.Include(er => er.Receiver)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Histories.Commands;
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ public class MappingProfile: Profile
|
|||||||
CreateMap<CreateHistoryCommand, History>()
|
CreateMap<CreateHistoryCommand, History>()
|
||||||
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
.ForMember(dest => dest.Envelope, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.Sender, opt => opt.Ignore())
|
.ForMember(dest => dest.Sender, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.Receiver, opt => opt.Ignore())
|
.ForMember(dest => dest.Receiver, opt => opt.Ignore());
|
||||||
.MapAddedWhen();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
|
||||||
|
|
||||||
//TODO: Add sender query
|
|
||||||
/// <summary>
|
|
||||||
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
|
|
||||||
/// </summary>
|
|
||||||
public record CountHistoryQuery : HistoryQueryBase, IRequest<int>;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class CountHistoryQueryExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender"></param>
|
|
||||||
/// <param name="uuid"></param>
|
|
||||||
/// <param name="statuses"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<bool> AnyHistoryAsync(this ISender sender, string uuid, IEnumerable<EnvelopeStatus> statuses, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var count = await sender.Send(new CountHistoryQuery
|
|
||||||
{
|
|
||||||
Envelope = new() { Uuid = uuid },
|
|
||||||
Statuses = new() { Include = statuses }
|
|
||||||
}, cancel);
|
|
||||||
return count > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class CountHistoryQueryHandler : IRequestHandler<CountHistoryQuery, int>
|
|
||||||
{
|
|
||||||
private readonly IRepository<History> _repo;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repo"></param>
|
|
||||||
public CountHistoryQueryHandler(IRepository<History> repo)
|
|
||||||
{
|
|
||||||
_repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="NotFoundException"></exception>
|
|
||||||
public Task<int> Handle(CountHistoryQuery request, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var query = _repo.Query;
|
|
||||||
|
|
||||||
if (request.Envelope.Id is int envId)
|
|
||||||
query = query.Where(e => e.Id == envId);
|
|
||||||
else if (request.Envelope.Uuid is string uuid)
|
|
||||||
query = query.Where(e => e.Envelope!.Uuid == uuid);
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
else if (request.EnvelopeId is not null)
|
|
||||||
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
else
|
|
||||||
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
|
|
||||||
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
if (request.Status is not null)
|
|
||||||
query = query.Where(h => h.Status == request.Status);
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
if (request.Statuses is not null)
|
|
||||||
{
|
|
||||||
var status = request.Statuses;
|
|
||||||
if (status.Min is not null)
|
|
||||||
query = query.Where(er => er.Envelope!.Status >= status.Min);
|
|
||||||
|
|
||||||
if (status.Max is not null)
|
|
||||||
query = query.Where(er => er.Envelope!.Status <= status.Max);
|
|
||||||
|
|
||||||
if (status.Include?.Count() > 0)
|
|
||||||
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
|
|
||||||
|
|
||||||
if (status.Ignore is not null)
|
|
||||||
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.CountAsync(cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Query;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
|
||||||
|
|
||||||
//TODO: Add sender query
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record HistoryQueryBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Die eindeutige Kennung des Umschlags.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use Envelope property")]
|
|
||||||
public int? EnvelopeId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use statuses")]
|
|
||||||
public EnvelopeStatus? Status { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public EnvelopeStatusQuery Statuses { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public EnvelopeQueryBase Envelope { get; set; } = new EnvelopeQueryBase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public record EnvelopeStatusQuery
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Der minimale Statuswert, der berücksichtigt werden.
|
|
||||||
/// </summary>
|
|
||||||
public EnvelopeStatus? Min { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Der maximale Statuswert, der berücksichtigt werden.
|
|
||||||
/// </summary>
|
|
||||||
public EnvelopeStatus? Max { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Eine Liste von Statuswerten, die einbezogen werden.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<EnvelopeStatus>? Include { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Eine Liste von Statuswerten, die ignoriert werden werden.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<EnvelopeStatus>? Ignore { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
using AutoMapper;
|
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||||
|
|
||||||
@@ -12,81 +9,21 @@ namespace EnvelopeGenerator.Application.Histories.Queries;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
|
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadHistoryQuery : HistoryQueryBase, IRequest<IEnumerable<HistoryDto>>
|
public record ReadHistoryQuery : IRequest<IEnumerable<HistoryDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Die eindeutige Kennung des Umschlags.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
|
||||||
|
/// </summary>
|
||||||
|
public EnvelopeStatus? Status { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage zur Steuerung, ob nur der aktuelle Include oder der gesamte Datensatz zurückgegeben wird.
|
/// Abfrage zur Steuerung, ob nur der aktuelle Include oder der gesamte Datensatz zurückgegeben wird.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OnlyLast { get; init; } = true;
|
public bool? OnlyLast { get; init; } = true;
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
|
|
||||||
{
|
|
||||||
private readonly IRepository<History> _repo;
|
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repo"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
|
|
||||||
{
|
|
||||||
_repo = repo;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="NotFoundException"></exception>
|
|
||||||
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var query = _repo.Query;
|
|
||||||
|
|
||||||
if (request.Envelope.Id is int envId)
|
|
||||||
query = query.Where(e => e.Id == envId);
|
|
||||||
else if (request.Envelope.Uuid is string uuid)
|
|
||||||
query = query.Where(e => e.Envelope!.Uuid == uuid);
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
else if (request.EnvelopeId is not null)
|
|
||||||
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
else
|
|
||||||
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
|
|
||||||
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
if (request.Status is not null)
|
|
||||||
query = query.Where(h => h.Status == request.Status);
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
if (request.Statuses is not null)
|
|
||||||
{
|
|
||||||
var status = request.Statuses;
|
|
||||||
if (status.Min is not null)
|
|
||||||
query = query.Where(er => er.Envelope!.Status >= status.Min);
|
|
||||||
|
|
||||||
if (status.Max is not null)
|
|
||||||
query = query.Where(er => er.Envelope!.Status <= status.Max);
|
|
||||||
|
|
||||||
if (status.Include?.Count() > 0)
|
|
||||||
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
|
|
||||||
|
|
||||||
if (status.Ignore is not null)
|
|
||||||
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.OnlyLast)
|
|
||||||
query = query.OrderByDescending(x => x.AddedWhen);
|
|
||||||
|
|
||||||
var hists = await query.ToListAsync(cancel);
|
|
||||||
return _mapper.Map<List<HistoryDto>>(hists);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
|
||||||
|
{
|
||||||
|
private readonly IRepository<History> _repo;
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repo"></param>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotFoundException"></exception>
|
||||||
|
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var query = _repo.ReadOnly().Where(h => h.EnvelopeId == request.EnvelopeId);
|
||||||
|
if (request.Status is not null)
|
||||||
|
query = query.Where(h => h.Status == request.Status);
|
||||||
|
|
||||||
|
var hists = await query.ToListAsync(cancel);
|
||||||
|
return _mapper.Map<List<HistoryDto>>(hists);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
namespace EnvelopeGenerator.Application.Interfaces.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
namespace EnvelopeGenerator.Application.Interfaces.Repositories;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user