Compare commits
100 Commits
11c8886d6f
...
bugfix/sav
| Author | SHA1 | Date | |
|---|---|---|---|
| d0c3d79fbc | |||
| e81c7896a1 | |||
| 127e2b253e | |||
| 6d0b78072e | |||
| 3721ea9a1f | |||
| f86588551c | |||
| 0eaa9ff08f | |||
| ec79d7622e | |||
| f558d03bdf | |||
| db79053c62 | |||
| 85dd85aa92 | |||
| c7bc9fbcca | |||
| 1e8e5c96c4 | |||
| 61d04ca51a | |||
| af674a811d | |||
| 123889451f | |||
| 3b5075fe44 | |||
| 48302a374f | |||
| b2c245d6bd | |||
| e9c0ec139a | |||
| 819bdcfcfb | |||
| ad7686b7c2 | |||
| ade542ded0 | |||
| 01c1283622 | |||
| d969dbe843 | |||
| 41779a4f58 | |||
| bf739c1360 | |||
| 0f167d731e | |||
| 021d7cfe29 | |||
| e2322b41cd | |||
| 618451f0c1 | |||
| 96c7cd6e8a | |||
| 4047ba7c6b | |||
| 8386b85592 | |||
| 640f0b95fe | |||
| 181f7f4dec | |||
| 95961a296f | |||
| db613358d5 | |||
| 7a126050cf | |||
| 7081c5f64e | |||
| 12ec5b28fa | |||
| 925e60a45c | |||
| 897860b2bf | |||
| 8933cff558 | |||
| b0b9ba7212 | |||
| 33c75ea7e1 | |||
| 513efa0833 | |||
| 0c81d79a9b | |||
| 696cd1b623 | |||
| 2434f67627 | |||
| 776700b247 | |||
| 7a114f47a6 | |||
| ed44d841e7 | |||
| 4bd0cc2e94 | |||
| 8a4963472c | |||
| 4d3f72a3ed | |||
| 1ebd43e3b0 | |||
| 19aeda5aef | |||
| 940d129c11 | |||
| 44bbdbb835 | |||
| 223017006b | |||
| ad9250c7b1 | |||
| 5cc47ca9c7 | |||
| 156988403a | |||
| e960c02cd4 | |||
| a0e2492757 | |||
| 380a5f4c0c | |||
| 28f8b829e3 | |||
| d00c459124 | |||
| 287c14e114 | |||
| 212ba9d569 | |||
| 735f072dd1 | |||
| 0f53d0509e | |||
| 48cfd3f416 | |||
| 0d2e43b2d6 | |||
| 5aaa37b8ec | |||
| 051a8ce211 | |||
| 519a8bf3f9 | |||
| a7d0b40e39 | |||
| 3dc9a180d3 | |||
| 4dba965c75 | |||
| 4d2538cc2c | |||
| d201eacc97 | |||
| 9daa671655 | |||
| 3fbf011fe3 | |||
| a815705fa4 | |||
| 6e44d0d4b5 | |||
| 42a853cb0d | |||
| b46c3c8ebe | |||
| 9f712a6b8c | |||
| 4449743105 | |||
| 435be882c9 | |||
| 09257d7766 | |||
| 16fa4b460f | |||
| f4aae8c787 | |||
| ed1468194d | |||
| f9f94c900e | |||
| 18df6221c9 | |||
| b15f95755c | |||
| 4be9e38a9e |
95
AGENTS.md
95
AGENTS.md
@@ -50,19 +50,6 @@ This section explains every markdown file in the repo: what it contains, why it
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Ticket / Feature Plans (Active Work Items)
|
|
||||||
|
|
||||||
#### `EnvelopeGenerator.Server/SF-74-PLAN.md`
|
|
||||||
**What:** Implementation plan for SF-74 ticket — Theme selection, Dark Mode, Grid Layout Persistence, signFLOW logo, and customer logo area on the sender dashboard. Includes the original ticket requirements, component breakdown, and implementation steps.
|
|
||||||
**Why it exists:** Captures the agreed scope and implementation decisions for SF-74 so work can be resumed or reviewed without re-reading the ticket system.
|
|
||||||
**When you need it:**
|
|
||||||
- Resuming work on theme, dark mode, or grid persistence features
|
|
||||||
- Checking what was agreed for logo positioning and size
|
|
||||||
- Verifying which features of SF-74 are complete vs. pending
|
|
||||||
**Maintenance:** Mark items as complete as they are implemented. Archive when the ticket is closed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Deployment Docs
|
### Deployment Docs
|
||||||
|
|
||||||
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
||||||
@@ -76,42 +63,15 @@ This section explains every markdown file in the repo: what it contains, why it
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Fix Reports (Historical — Read-Only)
|
|
||||||
|
|
||||||
These files document completed bug fixes. They exist as audit trails and as references if a similar issue resurfaces. Do not modify them.
|
|
||||||
|
|
||||||
#### `fix-report-label-read-and-confirmed.md`
|
|
||||||
**What:** Documents the fix for an incorrect label in the Signature Certificate report (`Signierungszertifikat`). For "Read and Sign" envelope type (`EnvelopeTypeId = 2`), the history event showed "Document signed" instead of "Read and confirmed" / "Gelesen und bestätigt".
|
|
||||||
**Branch:** `fix/report-label-read-and-confirmed` — **COMPLETED**
|
|
||||||
**When you need it:** If a similar report label regression appears, or when adding new envelope type-specific label logic to the certificate report.
|
|
||||||
|
|
||||||
#### `fix-signature-field-formatting.md`
|
|
||||||
**What:** Documents a signature stamp layout fix triggered by WISAG customer feedback (email 3/7/2026). The finalized PDF's signature stamp had incorrect spacing: certificate hash too close to name, position text wrapping, no spacing before date. Fix was applied in the GdPicture14 PDF burning pipeline — not in the Blazor UI.
|
|
||||||
**When you need it:** If signature stamp formatting regresses, or when modifying the PDF stamping/burning pipeline in the API or infrastructure layer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Service Migration Docs
|
|
||||||
|
|
||||||
#### `EnvelopeGenerator.Service/MIGRATION_PLAN.md`
|
|
||||||
**What:** Migration plan (v3) for porting `EnvelopeGenerator.Service_legacy` (VB.NET Windows Service) to `EnvelopeGenerator.Service` (C# Worker Service, .NET Framework 4.6.2). Documents connection string handling, job scheduling (Quartz → IHostedService), and deep analysis of legacy code.
|
|
||||||
**Why it exists:** The Windows Service handles background jobs (email dispatch, envelope finalization). This plan ensures the port is complete and no job logic is silently dropped.
|
|
||||||
**When you need it:**
|
|
||||||
- Modifying background jobs (email sending, PDF finalization, access code dispatch)
|
|
||||||
- Debugging service startup or job execution issues
|
|
||||||
- Checking if a specific legacy VB.NET job has been ported
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Active Architecture (Post-Migration)
|
## Active Architecture (Post-Migration)
|
||||||
|
|
||||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||||
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||||
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||||
|
|
||||||
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0)
|
**Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
|
||||||
|
|
||||||
**Proxy:** YARP in EnvelopeGenerator.Server routes `/api/*` → `localhost:8088` (API)
|
**Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
|
||||||
|
|
||||||
### Deprecated Projects - DO NOT USE
|
### Deprecated Projects - DO NOT USE
|
||||||
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
||||||
@@ -120,18 +80,14 @@ These files document completed bug fixes. They exist as audit trails and as refe
|
|||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Run Both Projects (Required)
|
### Run Active Host
|
||||||
```powershell
|
```powershell
|
||||||
# Terminal 1 - API Backend
|
# Terminal - Blazor + Web API host
|
||||||
cd EnvelopeGenerator.API
|
|
||||||
dotnet run
|
|
||||||
|
|
||||||
# Terminal 2 - Blazor Frontend
|
|
||||||
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
|
|
||||||
**Critical:** Both must run simultaneously. EnvelopeGenerator.Server proxy forwards `/api/*` to API.
|
**Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
```powershell
|
```powershell
|
||||||
@@ -144,10 +100,10 @@ dotnet build EnvelopeGenerator.sln
|
|||||||
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
|
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
|
||||||
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
||||||
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
||||||
EnvelopeGenerator.API/ # Controllers, endpoints
|
|
||||||
EnvelopeGenerator.Server/
|
EnvelopeGenerator.Server/
|
||||||
EnvelopeGenerator.Server/ # Server-side Blazor components
|
EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
|
||||||
├─ Components/Pages/ # @rendermode InteractiveServer
|
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||||
|
├─ Controllers/ # Active Web API endpoints
|
||||||
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
||||||
├─ Pages/ # @rendermode InteractiveWebAssembly
|
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||||
├─ Services/ # HTTP API clients
|
├─ Services/ # HTTP API clients
|
||||||
@@ -196,15 +152,13 @@ float inches = (pixelX / canvasWidth) * pageWidthInches;
|
|||||||
- **Use case:** External API consumers
|
- **Use case:** External API consumers
|
||||||
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
|
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
|
||||||
|
|
||||||
### Missing Granular Endpoints (Need to Create)
|
### Sender Workflow Endpoint
|
||||||
```
|
```
|
||||||
POST /api/Envelope/draft # Create draft envelope
|
POST /api/Envelope # Create/update/save/send envelope workflow payload
|
||||||
PUT /api/Envelope/{id} # Update metadata
|
|
||||||
DELETE /api/Envelope/{id} # Delete with reason
|
DELETE /api/Envelope/{id} # Delete with reason
|
||||||
POST /api/Envelope/{id}/document # Upload PDF
|
POST /api/Envelope/{id}/document # Upload PDF
|
||||||
POST /api/Envelope/{id}/receivers # Add receiver
|
POST /api/Envelope/{id}/receivers # Add receiver
|
||||||
POST /api/Envelope/{id}/signature-fields # Place signature field
|
POST /api/Envelope/{id}/signature-fields # Place signature field
|
||||||
POST /api/Envelope/{id}/send # Send to receivers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
||||||
@@ -239,7 +193,7 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Config (`API/appsettings.json`)
|
### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
|
||||||
- `ConnectionStrings:Default` - SQL Server DB
|
- `ConnectionStrings:Default` - SQL Server DB
|
||||||
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
||||||
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
||||||
@@ -277,16 +231,30 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
|||||||
**No automated tests exist yet.**
|
**No automated tests exist yet.**
|
||||||
|
|
||||||
Manual testing workflow:
|
Manual testing workflow:
|
||||||
1. Start API (`dotnet run` in `EnvelopeGenerator.API`)
|
1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||||
2. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
2. Navigate to `https://localhost:5131` (or check console output for port)
|
||||||
3. Navigate to `https://localhost:5131` (or check console output for port)
|
3. Test sender login at `/sender/login`
|
||||||
4. Test sender login at `/sender/login`
|
4. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||||
5. Test receiver flow at `/envelope/login/{envelopeKey}`
|
|
||||||
|
## Technical Debt TODOs
|
||||||
|
|
||||||
|
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
|
||||||
|
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
|
||||||
|
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
|
||||||
|
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
|
||||||
|
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
|
||||||
|
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
|
||||||
|
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
|
||||||
|
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
|
||||||
|
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
|
||||||
|
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
||||||
|
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
||||||
|
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
**SQL Server** (DD_ECM)
|
**SQL Server** (DD_ECM)
|
||||||
- Connection string in `API/appsettings.json`
|
- Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
|
||||||
- EF Core migrations NOT used (manual SQL scripts)
|
- EF Core migrations NOT used (manual SQL scripts)
|
||||||
- Stored procedures: `PRSIG_*` prefix
|
- Stored procedures: `PRSIG_*` prefix
|
||||||
|
|
||||||
@@ -360,5 +328,4 @@ Each envelope maintains independent authentication state.
|
|||||||
None required. All config in `appsettings.json`.
|
None required. All config in `appsettings.json`.
|
||||||
|
|
||||||
**Local dev ports:**
|
**Local dev ports:**
|
||||||
- API: `https://localhost:8088`
|
- App host (UI + API): `https://localhost:5131` or `http://localhost:7192`
|
||||||
- WebUI: `https://localhost:5131` or `http://localhost:7192`
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ This project contains:
|
|||||||
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||||
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||||
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||||
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
- `EnvelopeGenerator.API` — deprecated project (no longer part of the active solution/runtime path)
|
||||||
|
|
||||||
### Legacy / Do Not Touch
|
### Legacy / Do Not Touch
|
||||||
- `EnvelopeGenerator.Service`
|
- `EnvelopeGenerator.Service`
|
||||||
@@ -121,7 +121,7 @@ Current controller set includes:
|
|||||||
- `SignatureController`
|
- `SignatureController`
|
||||||
- `TfaRegistrationController`
|
- `TfaRegistrationController`
|
||||||
|
|
||||||
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class AuthScheme
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Scheme name used for per-envelope receiver JWT authentication.
|
|
||||||
/// </summary>
|
|
||||||
public const string Receiver = "EnvelopeGenerator.API.ReceiverJWT";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scheme name used for per-envelope sender JWT authentication.
|
|
||||||
/// </summary>
|
|
||||||
public const string Sender = "EnvelopeGenerator.API.SenderJWT";
|
|
||||||
}
|
|
||||||
@@ -1,132 +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.Common.Notifications.RemoveSignature;
|
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
||||||
using EnvelopeGenerator.Application.Histories.Queries;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages annotations and signature lifecycle for envelopes.
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class AnnotationController : ControllerBase
|
|
||||||
{
|
|
||||||
[Obsolete("Use MediatR")]
|
|
||||||
private readonly IEnvelopeHistoryService _historyService;
|
|
||||||
|
|
||||||
[Obsolete("Use MediatR")]
|
|
||||||
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
|
||||||
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
private readonly ILogger<AnnotationController> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="AnnotationController"/>.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use MediatR")]
|
|
||||||
public AnnotationController(
|
|
||||||
ILogger<AnnotationController> logger,
|
|
||||||
IEnvelopeHistoryService envelopeHistoryService,
|
|
||||||
IEnvelopeReceiverService envelopeReceiverService,
|
|
||||||
IMediator mediator)
|
|
||||||
{
|
|
||||||
_historyService = envelopeHistoryService;
|
|
||||||
_envelopeReceiverService = envelopeReceiverService;
|
|
||||||
_mediator = mediator;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates or updates annotations for the authenticated envelope receiver.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
|
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpPost]
|
|
||||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
|
||||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var signature = User.ReceiverSignature();
|
|
||||||
var uuid = User.EnvelopeUuid();
|
|
||||||
|
|
||||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
|
||||||
|
|
||||||
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
|
|
||||||
return BadRequest();
|
|
||||||
|
|
||||||
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
|
||||||
return Problem(statusCode: StatusCodes.Status409Conflict);
|
|
||||||
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
|
||||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
|
||||||
|
|
||||||
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
|
||||||
var docSignedNotification = envelopeReceiverDto is not null
|
|
||||||
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
|
||||||
: throw new NotFoundException("Envelope receiver is not found.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _mediator.Publish(docSignedNotification, cancel);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await _mediator.Publish(new RemoveSignatureNotification()
|
|
||||||
{
|
|
||||||
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
|
|
||||||
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
|
|
||||||
}, cancel);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rejects the document for the current receiver.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reason">Optional rejection reason.</param>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpPost("reject")]
|
|
||||||
[Obsolete("Use MediatR")]
|
|
||||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
|
||||||
{
|
|
||||||
var signature = User.ReceiverSignature();
|
|
||||||
var uuid = User.EnvelopeUuid();
|
|
||||||
var mail = User.ReceiverMail();
|
|
||||||
|
|
||||||
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
|
||||||
|
|
||||||
if (envRcvRes.IsFailed)
|
|
||||||
{
|
|
||||||
_logger.LogNotice(envRcvRes.Notices);
|
|
||||||
return Unauthorized("you are not authorized");
|
|
||||||
}
|
|
||||||
|
|
||||||
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
|
|
||||||
if (histRes.IsSuccess)
|
|
||||||
{
|
|
||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
|
|
||||||
_logger.LogNotice(histRes.Notices);
|
|
||||||
return StatusCode(500, histRes.Messages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(AuthenticationSchemes = AuthScheme.Sender)]
|
|
||||||
[HttpPost("logout")]
|
|
||||||
public async Task<IActionResult> Logout()
|
|
||||||
{
|
|
||||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
|
||||||
Response.Cookies.Delete(authTokenKeys.Cookie);
|
|
||||||
else if (await this.IsUserInPolicyAsync(AuthPolicy.ReceiverOrReceiverTFA))
|
|
||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
else
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prüft, ob der Benutzer ein autorisiertes Token hat.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Sample request:
|
|
||||||
///
|
|
||||||
/// GET /api/auth
|
|
||||||
///
|
|
||||||
/// </remarks>
|
|
||||||
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
|
|
||||||
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
|
||||||
[HttpGet("check")]
|
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
|
||||||
public IActionResult Check(string? role = null)
|
|
||||||
=> role is not null && !User.IsInRole(role)
|
|
||||||
? Unauthorized()
|
|
||||||
: Ok();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key.
|
|
||||||
/// The request must carry a cookie named <c>AuthTokenSignFLOWReceiver.{envelopeKey}</c>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeKey">The unique envelope key extracted from the route.</param>
|
|
||||||
/// <response code="200">Valid per-envelope token found.</response>
|
|
||||||
/// <response code="401">Token is missing, expired or invalid.</response>
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpGet("check/envelope/{envelopeKey}")]
|
|
||||||
public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes the per-envelope receiver cookie for the given envelope key.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeKey">The unique envelope key whose cookie should be deleted.</param>
|
|
||||||
/// <response code="200">Cookie successfully deleted.</response>
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
|
||||||
[HttpPost("logout/envelope/{envelopeKey}")]
|
|
||||||
public IActionResult LogoutEnvelopeReceiver([FromRoute] string envelopeKey)
|
|
||||||
{
|
|
||||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
|
||||||
Response.Cookies.Delete(cookieName);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes all per-envelope receiver cookies from the current request.
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">All envelope receiver cookies successfully deleted.</response>
|
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
|
||||||
[HttpPost("logout/envelope")]
|
|
||||||
public IActionResult LogoutAllEnvelopeReceivers()
|
|
||||||
{
|
|
||||||
foreach (var cookieName in Request.Cookies.Keys.Where(k => CookieNames.IsEnvelopeReceiverCookie(k, authTokenKeys.Cookie)))
|
|
||||||
Response.Cookies.Delete(cookieName);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.Text.Json;
|
|
||||||
using EnvelopeGenerator.API.Options;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.API.Extensions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages cached data for receivers using distributed cache.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
public class CacheController(
|
|
||||||
IDistributedCache cache,
|
|
||||||
IOptions<CacheOptions> cacheOptions) : ControllerBase
|
|
||||||
{
|
|
||||||
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores a receiver's signature in cache for the specified envelope.
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpPost("SignatureCapture/{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> SaveSignature(
|
|
||||||
[FromRoute] string envelopeKey,
|
|
||||||
[FromBody] SignatureCacheRequest request,
|
|
||||||
CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
|
||||||
var json = JsonSerializer.Serialize(request);
|
|
||||||
|
|
||||||
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
|
|
||||||
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
|
|
||||||
: null;
|
|
||||||
|
|
||||||
await cache.SetStringAsync(cacheKey, json, options ?? new DistributedCacheEntryOptions(), cancel);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a cached signature for the specified envelope.
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpGet("SignatureCapture/{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> GetSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
|
||||||
var json = await cache.GetStringAsync(cacheKey, cancel);
|
|
||||||
|
|
||||||
if (json is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var signature = JsonSerializer.Deserialize<SignatureCacheRequest>(json);
|
|
||||||
return Ok(signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes a cached signature for the specified envelope.
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpDelete("SignatureCapture/{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
|
||||||
await cache.RemoveAsync(cacheKey, cancel);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request model for caching signature data.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record SignatureCacheRequest(
|
|
||||||
string DataUrl,
|
|
||||||
string FullName,
|
|
||||||
string Place,
|
|
||||||
string? Position = null);
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Exposes configuration data required by the client applications.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Initializes a new instance of <see cref="ConfigController"/>.
|
|
||||||
/// </remarks>
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
|
||||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns annotation configuration that was previously rendered by MVC.
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("Annotations")]
|
|
||||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
|
||||||
public IActionResult GetAnnotationParams()
|
|
||||||
{
|
|
||||||
return Ok(_annotationParams.AnnotationJSObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 DocReceiverElementController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DocReceiverElementController(IMediator mediator)
|
|
||||||
{
|
|
||||||
_mediator = mediator;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: update to use signature query
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeKey"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpGet("{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
int envelopeId = User.EnvelopeId();
|
|
||||||
|
|
||||||
int receiverId = User.ReceiverId();
|
|
||||||
|
|
||||||
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
|
||||||
|
|
||||||
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
|
|
||||||
return NotFound("Document is empty.");
|
|
||||||
|
|
||||||
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
|
|
||||||
|
|
||||||
if (rcvSignatures is null)
|
|
||||||
return NotFound("No signatures found for the current receiver.");
|
|
||||||
else
|
|
||||||
return Ok(rcvSignatures);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.EnvelopeId();
|
|
||||||
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
|
|
||||||
return receiverDoc.ByteData is byte[] receiverDocByte
|
|
||||||
? File(receiverDocByte, "application/octet-stream")
|
|
||||||
: NotFound("Document is empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the document for the specified envelope key.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeKey"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpGet("{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
int envelopeId = User.EnvelopeId();
|
|
||||||
|
|
||||||
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
|
||||||
|
|
||||||
if (senderDoc.ByteData is not byte[] senderDocByte)
|
|
||||||
return NotFound("Document is empty.");
|
|
||||||
|
|
||||||
Response.Headers.ContentDisposition = $"inline; filename=\"{envelopeKey}.pdf\"";
|
|
||||||
return File(senderDocByte, "application/pdf");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(AuthenticationSchemes = AuthScheme.Sender)]
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
|
||||||
{
|
|
||||||
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
|
|
||||||
return result.Any() ? Ok(result) : NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query"></param>
|
|
||||||
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
|
|
||||||
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
|
|
||||||
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
|
|
||||||
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
|
|
||||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
|
||||||
[HttpGet("doc-result")]
|
|
||||||
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
|
||||||
{
|
|
||||||
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
|
|
||||||
var envelope = envelopes.FirstOrDefault();
|
|
||||||
|
|
||||||
if (envelope is null)
|
|
||||||
return NotFound("Envelope not available.");
|
|
||||||
if (envelope.DocResult is null)
|
|
||||||
return NotFound("The document has not been fully signed or the result has not yet been released.");
|
|
||||||
|
|
||||||
if (view)
|
|
||||||
{
|
|
||||||
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
|
|
||||||
return File(envelope.DocResult, "application/pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="command"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[NonAction]
|
|
||||||
[Authorize]
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
|
||||||
{
|
|
||||||
var res = await _mediator.Send(command.WithAuth(User.GetId()));
|
|
||||||
|
|
||||||
if (res is null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return Ok(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
using AutoMapper;
|
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
||||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using EnvelopeGenerator.API.Models;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Data.SqlClient;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.Data;
|
|
||||||
using EnvelopeGenerator.Application.Common.SQL;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
|
||||||
using EnvelopeGenerator.API.Extensions;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controller für die Verwaltung von Umschlagempfängern.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
|
|
||||||
/// </remarks>
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[Authorize]
|
|
||||||
[ApiController]
|
|
||||||
public class EnvelopeReceiverController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly ILogger<EnvelopeReceiverController> _logger;
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly IEnvelopeExecutor _envelopeExecutor;
|
|
||||||
private readonly IEnvelopeReceiverExecutor _erExecutor;
|
|
||||||
private readonly IDocumentExecutor _documentExecutor;
|
|
||||||
private readonly string _cnnStr;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Konstruktor für den EnvelopeReceiverController.
|
|
||||||
/// </summary>
|
|
||||||
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_mediator = mediator;
|
|
||||||
_mapper = mapper;
|
|
||||||
_envelopeExecutor = envelopeExecutor;
|
|
||||||
_erExecutor = erExecutor;
|
|
||||||
_documentExecutor = documentExecutor;
|
|
||||||
_cnnStr = csOpt.Value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeReceiver">Die Abfrageparameter für die Filterung von Umschlagempfängern.</param>
|
|
||||||
/// <returns>Eine HTTP-Antwort mit der Liste der gefundenen Umschlagempfänger oder einem Fehlerstatus.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Dieser Endpunkt ermöglicht es, Umschlagempfänger basierend auf dem Benutzernamen und optionalen Statusfiltern abzurufen.
|
|
||||||
/// Wenn der Benutzername nicht ermittelt werden kann, wird ein Serverfehler zurückgegeben.
|
|
||||||
/// </remarks>
|
|
||||||
/// <response code="200">Die Liste der Umschlagempfänger wurde erfolgreich abgerufen.</response>
|
|
||||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
|
||||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
|
|
||||||
{
|
|
||||||
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
|
|
||||||
|
|
||||||
var result = await _mediator.Send(envelopeReceiver);
|
|
||||||
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeKey"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpGet("{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var er = await _mediator.Send(new ReadEnvelopeReceiverQuery()
|
|
||||||
{
|
|
||||||
Key = envelopeKey
|
|
||||||
}, cancel);
|
|
||||||
|
|
||||||
return Ok(er.SingleOrDefault());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="receiver">Abfrage, bei der nur eine der Angaben ID, Signatur oder E-Mail-Adresse des Empfängers eingegeben werden muss.</param>
|
|
||||||
/// <returns>Eine HTTP-Antwort mit dem Namen des Empfängers oder einem Fehlerstatus.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Dieser Endpunkt ermöglicht es, den Namen des zuletzt verwendeten Empfängers basierend auf der E-Mail-Adresse abzurufen.
|
|
||||||
/// </remarks>
|
|
||||||
/// <response code="200">Der Name des Empfängers wurde erfolgreich abgerufen.</response>
|
|
||||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
|
||||||
/// <response code="404">Kein Empfänger gefunden.</response>
|
|
||||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet("salute")]
|
|
||||||
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
|
||||||
{
|
|
||||||
var name = await _mediator.Send(receiver);
|
|
||||||
return name is null ? NotFound() : Ok(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns>HTTP-Antwort</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Sample request:
|
|
||||||
///
|
|
||||||
/// POST /api/envelope
|
|
||||||
/// {
|
|
||||||
/// "title": "Vertragsdokument",
|
|
||||||
/// "message": "Bitte unterschreiben Sie dieses Dokument.",
|
|
||||||
/// "document": {
|
|
||||||
/// "dataAsBase64": "dGVzdC1iYXNlNjQtZGF0YQ=="
|
|
||||||
/// },
|
|
||||||
/// "receivers": [
|
|
||||||
/// {
|
|
||||||
/// "emailAddress": "example@example.com",
|
|
||||||
/// "signatures": [
|
|
||||||
/// {
|
|
||||||
/// "x": 100,
|
|
||||||
/// "y": 200,
|
|
||||||
/// "page": 1
|
|
||||||
/// }
|
|
||||||
/// ],
|
|
||||||
/// "name": "Max Mustermann",
|
|
||||||
/// "phoneNumber": "+49123456789"
|
|
||||||
/// }
|
|
||||||
/// ],
|
|
||||||
/// "tfaEnabled": false
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// </remarks>
|
|
||||||
/// <response code="202">Envelope-Erstellung und Sendeprozessbefehl erfolgreich</response>
|
|
||||||
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
|
||||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
|
||||||
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
|
||||||
[Authorize]
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
#region Create Envelope
|
|
||||||
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Add receivers
|
|
||||||
List<EnvelopeReceiver> sentReceivers = new();
|
|
||||||
List<ReceiverGetOrCreateCommand> unsentReceivers = new();
|
|
||||||
|
|
||||||
foreach (var receiver in request.Receivers)
|
|
||||||
{
|
|
||||||
var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
|
||||||
|
|
||||||
if (envelopeReceiver is null)
|
|
||||||
unsentReceivers.Add(receiver);
|
|
||||||
else
|
|
||||||
sentReceivers.Add(envelopeReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
var res = _mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
|
||||||
res.UnsentReceivers = unsentReceivers;
|
|
||||||
res.SentReceiver = _mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Add document
|
|
||||||
var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
|
||||||
|
|
||||||
if (document is null)
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Add document element
|
|
||||||
// @DOC_ID, @RECEIVER_ID, @POSITION_X, @POSITION_Y, @PAGE
|
|
||||||
string sql = @"
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_DOC_RECEIVER_ELEM]
|
|
||||||
{0},
|
|
||||||
{1},
|
|
||||||
{2},
|
|
||||||
{3},
|
|
||||||
{4},
|
|
||||||
@OUT_SUCCESS OUTPUT;
|
|
||||||
|
|
||||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
|
||||||
|
|
||||||
foreach (var rcv in res.SentReceiver)
|
|
||||||
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.DocReceiverElementCreateDto>())
|
|
||||||
{
|
|
||||||
using SqlConnection conn = new(_cnnStr);
|
|
||||||
conn.Open();
|
|
||||||
|
|
||||||
var formattedSQL = string.Format(sql, document.Id.ToSqlParam(), rcv.Id.ToSqlParam(), sign.X.ToSqlParam(), sign.Y.ToSqlParam(), sign.Page.ToSqlParam());
|
|
||||||
|
|
||||||
using SqlCommand cmd = new(formattedSQL, conn);
|
|
||||||
cmd.CommandType = CommandType.Text;
|
|
||||||
|
|
||||||
using SqlDataReader reader = cmd.ExecuteReader();
|
|
||||||
if (reader.Read())
|
|
||||||
{
|
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
|
||||||
if (!outSuccess)
|
|
||||||
_logger.LogWarning(
|
|
||||||
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
|
||||||
document.Id, rcv.Id, sign.Page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Create history
|
|
||||||
// ENV_UID, STATUS_ID, USER_ID,
|
|
||||||
string sql_hist = @"
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
|
||||||
{0},
|
|
||||||
{1},
|
|
||||||
{2},
|
|
||||||
@OUT_SUCCESS OUTPUT;
|
|
||||||
|
|
||||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
|
||||||
|
|
||||||
using (SqlConnection conn = new(_cnnStr))
|
|
||||||
{
|
|
||||||
conn.Open();
|
|
||||||
var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), User.GetId().ToSqlParam());
|
|
||||||
using SqlCommand cmd = new(formattedSQL_hist, conn);
|
|
||||||
cmd.CommandType = CommandType.Text;
|
|
||||||
|
|
||||||
using SqlDataReader reader = cmd.ExecuteReader();
|
|
||||||
if (reader.Read())
|
|
||||||
{
|
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
|
||||||
if (!outSuccess)
|
|
||||||
_logger.LogWarning(
|
|
||||||
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
|
||||||
envelope.Uuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
return Ok(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool IsBase64String(string input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Convert.FromBase64String(input);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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,118 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using EnvelopeGenerator.Application.Histories.Queries;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
|
|
||||||
/// </summary>
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class HistoryController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IMemoryCache _memoryCache;
|
|
||||||
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Konstruktor für den HistoryController.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="memoryCache"></param>
|
|
||||||
/// <param name="mediator"></param>
|
|
||||||
public HistoryController(IMemoryCache memoryCache, IMediator mediator)
|
|
||||||
{
|
|
||||||
_memoryCache = memoryCache;
|
|
||||||
_mediator = mediator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gibt alle möglichen Verweise auf alle möglichen Include in einem Verlaufsdatensatz zurück. (z. B. DocumentSigned bezieht sich auf Receiver.)
|
|
||||||
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
|
||||||
/// 1 - Sender:
|
|
||||||
/// Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 1* beginnen.
|
|
||||||
/// 2 - Receiver:
|
|
||||||
/// Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 2* beginnen.
|
|
||||||
/// 3 - System:
|
|
||||||
/// Historische Datensätze, die sich auf den allgemeinen Zustand des Umschlags beziehen. Diese haben Statuscodes, die mit 3* beginnen.
|
|
||||||
/// 4 - Unknown:
|
|
||||||
/// Ein unbekannter Datensatz weist auf einen möglichen Mangel oder eine Unstimmigkeit im Aktualisierungsprozess der Anwendung hin.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("related")]
|
|
||||||
[Authorize]
|
|
||||||
public IActionResult GetReferenceTypes(ReferenceType? referenceType = null)
|
|
||||||
{
|
|
||||||
return referenceType is null
|
|
||||||
? Ok(_memoryCache.GetEnumAsDictionary<ReferenceType>("gen.api", ReferenceType.Unknown))
|
|
||||||
: Ok(referenceType.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gibt alle möglichen Include in einem Verlaufsdatensatz zurück.
|
|
||||||
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
|
||||||
/// 1003: EnvelopeQueued
|
|
||||||
/// 1006: EnvelopeCompletelySigned
|
|
||||||
/// 1007: EnvelopeReportCreated
|
|
||||||
/// 1008: EnvelopeArchived
|
|
||||||
/// 1009: EnvelopeDeleted
|
|
||||||
/// 10007: EnvelopeRejected
|
|
||||||
/// 10009: EnvelopeWithdrawn
|
|
||||||
/// 2001: AccessCodeRequested
|
|
||||||
/// 2002: AccessCodeCorrect
|
|
||||||
/// 2003: AccessCodeIncorrect
|
|
||||||
/// 2004: DocumentOpened
|
|
||||||
/// 2005: DocumentSigned
|
|
||||||
/// 2006: DocumentForwarded
|
|
||||||
/// 2007: DocumentRejected
|
|
||||||
/// 2008: EnvelopeShared
|
|
||||||
/// 2009: EnvelopeViewed
|
|
||||||
/// 3001: MessageInvitationSent (Wird von Trigger verwendet)
|
|
||||||
/// 3002: MessageAccessCodeSent
|
|
||||||
/// 3003: MessageConfirmationSent
|
|
||||||
/// 3004: MessageDeletionSent
|
|
||||||
/// 3005: MessageCompletionSent
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="status">
|
|
||||||
/// Abfrageparameter, der angibt, auf welche Referenz sich der Include bezieht.
|
|
||||||
/// 1 - Sender: Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 1* beginnen.
|
|
||||||
/// 2 - Receiver: Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 2* beginnen.
|
|
||||||
/// 3 - System: Diese werden durch Datenbank-Trigger aktualisiert und sind in den Tabellen EnvelopeHistory und EmailOut zu finden.Sie arbeiten
|
|
||||||
/// integriert mit der Anwendung EmailProfiler, um E-Mails zu versenden und haben die Codes, die mit 3* beginnen.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>Gibt die HTTP-Antwort zurück.</returns>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("status")]
|
|
||||||
[Authorize]
|
|
||||||
public IActionResult GetEnvelopeStatus([FromQuery] EnvelopeStatus? status = null)
|
|
||||||
{
|
|
||||||
return status is null
|
|
||||||
? Ok(_memoryCache.GetEnumAsDictionary<EnvelopeStatus>("gen.api", Status.NonHist, Status.RelatedToFormApp))
|
|
||||||
: Ok(status.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ruft die gesamte Umschlaghistorie basierend auf den angegebenen Abfrageparametern ab.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="historyQuery">Die Abfrageparameter, die die Filterkriterien für die Umschlaghistorie definieren.</param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns>Eine Liste von Historieneinträgen, die den angegebenen Kriterien entsprechen, oder nur der letzte Eintrag.</returns>
|
|
||||||
/// <response code="200">Die Anfrage war erfolgreich, und die Umschlaghistorie wird zurückgegeben.</response>
|
|
||||||
/// <response code="400">Die Anfrage war ungültig oder unvollständig.</response>
|
|
||||||
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
|
||||||
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
|
||||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> GetAllAsync([FromQuery] ReadHistoryQuery historyQuery, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var history = await _mediator.Send(historyQuery, cancel).ThrowIfEmpty(Exceptions.NotFound);
|
|
||||||
return Ok((historyQuery.OnlyLast) ? history.MaxBy(h => h.AddedWhen) : history);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,121 +0,0 @@
|
|||||||
using DigitalData.Core.API;
|
|
||||||
using EnvelopeGenerator.Application.Resources;
|
|
||||||
using Microsoft.AspNetCore.Localization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using EnvelopeGenerator.Application.Resources;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
|
|
||||||
/// </summary>
|
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[ApiController]
|
|
||||||
public class LocalizationController : ControllerBase
|
|
||||||
{
|
|
||||||
private static readonly Guid L_KEY = Guid.NewGuid();
|
|
||||||
|
|
||||||
private readonly ILogger<LocalizationController> _logger;
|
|
||||||
private readonly IStringLocalizer<Resource> _mLocalizer;
|
|
||||||
private readonly IStringLocalizer<Resource> _localizer;
|
|
||||||
private readonly IMemoryCache _cache;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Konstruktor für den <see cref="LocalizationController"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Logger für die Protokollierung.</param>
|
|
||||||
/// <param name="localizer">Lokalisierungsdienst für Ressourcen.</param>
|
|
||||||
/// <param name="memoryCache">Speicher-Cache für die Zwischenspeicherung von Daten.</param>
|
|
||||||
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
|
|
||||||
public LocalizationController(
|
|
||||||
ILogger<LocalizationController> logger,
|
|
||||||
IStringLocalizer<Resource> localizer,
|
|
||||||
IMemoryCache memoryCache,
|
|
||||||
IStringLocalizer<Resource> _modelLocalizer)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_localizer = localizer;
|
|
||||||
_cache = memoryCache;
|
|
||||||
_mLocalizer = _modelLocalizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ruft alle lokalisierten Daten ab.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Eine Liste aller lokalisierten Daten.</returns>
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult GetAll() => Ok(_cache.GetOrCreate(Language ?? string.Empty + L_KEY, _ => _mLocalizer.ToDictionary()));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ruft die aktuelle Sprache ab.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Die aktuelle Sprache oder ein NotFound-Ergebnis, wenn keine Sprache gesetzt ist.</returns>
|
|
||||||
[HttpGet("lang")]
|
|
||||||
public IActionResult GetLanguage() => Language is null ? NotFound() : Ok(Language);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setzt die Sprache.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="language">Die zu setzende Sprache.</param>
|
|
||||||
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gesetzt wurde, oder ein BadRequest-Ergebnis, wenn die Eingabe ungültig ist.</returns>
|
|
||||||
[HttpPost("lang")]
|
|
||||||
public IActionResult SetLanguage([FromQuery] string language)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(language))
|
|
||||||
return BadRequest();
|
|
||||||
|
|
||||||
Language = language;
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Löscht die aktuelle Sprache.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gelöscht wurde.</returns>
|
|
||||||
[HttpDelete("lang")]
|
|
||||||
public IActionResult DeleteLanguage()
|
|
||||||
{
|
|
||||||
Language = null;
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Eigenschaft für die Verwaltung der aktuellen Sprache über Cookies.
|
|
||||||
/// </summary>
|
|
||||||
private string? Language
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(cookieValue))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
|
|
||||||
return culture?.Value ?? null;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value is null)
|
|
||||||
Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var cookieOptions = new CookieOptions()
|
|
||||||
{
|
|
||||||
Expires = DateTimeOffset.UtcNow.AddYears(1),
|
|
||||||
Secure = false,
|
|
||||||
SameSite = SameSiteMode.Strict,
|
|
||||||
HttpOnly = true
|
|
||||||
};
|
|
||||||
|
|
||||||
Response.Cookies.Append(
|
|
||||||
CookieRequestCultureProvider.DefaultCookieName,
|
|
||||||
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)),
|
|
||||||
cookieOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.ReceiverMail();
|
|
||||||
if (authReceiverMail is null)
|
|
||||||
{
|
|
||||||
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
|
||||||
return Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
var envelopeId = User.EnvelopeId();
|
|
||||||
|
|
||||||
createDto.AddedWho = authReceiverMail;
|
|
||||||
createDto.EnvelopeId = envelopeId;
|
|
||||||
|
|
||||||
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
|
|
||||||
|
|
||||||
if (creationRes.IsFailed)
|
|
||||||
{
|
|
||||||
_logger.LogNotice(creationRes);
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
|
||||||
}
|
|
||||||
|
|
||||||
var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id);
|
|
||||||
if (readRes.IsFailed)
|
|
||||||
{
|
|
||||||
_logger.LogNotice(creationRes);
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
|
||||||
}
|
|
||||||
|
|
||||||
var newReadOnly = readRes.Data;
|
|
||||||
|
|
||||||
return await _mailService.SendAsync(newReadOnly).ThenAsync<int, IActionResult>(SuccessAsync: async _ =>
|
|
||||||
{
|
|
||||||
var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared);
|
|
||||||
if (histRes.IsFailed)
|
|
||||||
{
|
|
||||||
_logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto));
|
|
||||||
_logger.LogNotice(histRes.Notices);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
},
|
|
||||||
|
|
||||||
Fail: (msg, ntc) =>
|
|
||||||
{
|
|
||||||
_logger.LogNotice(ntc);
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,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,84 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<PackageId>EnvelopeGenerator.GeneratorAPI</PackageId>
|
|
||||||
<Title></Title>
|
|
||||||
<Authors>Digital Data GmbH</Authors>
|
|
||||||
<Company>Digital Data GmbH</Company>
|
|
||||||
<Product>EnvelopeGenerator.GeneratorAPI</Product>
|
|
||||||
<Version>1.4.0</Version>
|
|
||||||
<FileVersion>1.4.0</FileVersion>
|
|
||||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
|
||||||
<PackageOutputPath>Copyright © 2025 Digital Data GmbH. All rights reserved.</PackageOutputPath>
|
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Remove="ClientApp\**" />
|
|
||||||
<Content Remove="ClientApp\**" />
|
|
||||||
<EmbeddedResource Remove="ClientApp\**" />
|
|
||||||
<None Remove="ClientApp\**" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="yarp.json" CopyToOutputDirectory="PreserveNewest" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="AspNetCore.Scalar" Version="1.1.8" />
|
|
||||||
<PackageReference Include="DigitalData.Auth.Claims" Version="1.0.3" />
|
|
||||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
|
||||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.28" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
|
||||||
<PackageReference Include="itext" Version="8.0.5" />
|
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" Condition="'$(TargetFramework)' == 'net9.0'" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" Condition="'$(TargetFramework)' == 'net8.0'" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
|
||||||
<PackageReference Include="NLog" Version="5.2.5" />
|
|
||||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
|
||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.0" />
|
|
||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
|
||||||
<PackageReference Include="System.DirectoryServices" Version="7.0.1" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="7.0.1" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="7.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
|
||||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
|
||||||
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="wwwroot\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
using DigitalData.Auth.Claims;
|
|
||||||
using Microsoft.IdentityModel.JsonWebTokens;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides helper methods for working with envelope-specific authentication claims.
|
|
||||||
/// </summary>
|
|
||||||
public static class ReceiverClaimExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user"></param>
|
|
||||||
/// <param name="claimType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
private static string GetRequiredClaimValue(this ClaimsPrincipal user, string claimType)
|
|
||||||
{
|
|
||||||
var value = user.FindFirstValue(claimType);
|
|
||||||
if (value is not null)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var identity = user.Identity;
|
|
||||||
var principalName = identity?.Name ?? "(anonymous)";
|
|
||||||
var authType = identity?.AuthenticationType ?? "(none)";
|
|
||||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
|
||||||
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes)
|
|
||||||
{
|
|
||||||
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
|
|
||||||
{
|
|
||||||
var value = user.FindFirstValue(claimType);
|
|
||||||
if (!string.IsNullOrWhiteSpace(value))
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var identity = user.Identity;
|
|
||||||
var principalName = identity?.Name ?? "(anonymous)";
|
|
||||||
var authType = identity?.AuthenticationType ?? "(none)";
|
|
||||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
|
||||||
var message = $"Required claim(s) '{string.Join("', '", claimTypes)}' are missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope UUID from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string EnvelopeUuid(this ClaimsPrincipal user)
|
|
||||||
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver signature from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string ReceiverSignature(this ClaimsPrincipal user)
|
|
||||||
=> user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver email address from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string ReceiverMail(this ClaimsPrincipal user)
|
|
||||||
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope identifier from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static int EnvelopeId(this ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId);
|
|
||||||
if (int.TryParse(envIdStr, out var envId))
|
|
||||||
return envId;
|
|
||||||
else
|
|
||||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver identifier from the claims.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public static int ReceiverId(this ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
|
|
||||||
if (int.TryParse(rcvIdStr, out var rcvId))
|
|
||||||
return rcvId;
|
|
||||||
else
|
|
||||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
EnvelopeGenerator.API/Jenkinsfile
vendored
10
EnvelopeGenerator.API/Jenkinsfile
vendored
@@ -1,10 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
agent any
|
|
||||||
stages {
|
|
||||||
stage('Build') {
|
|
||||||
steps {
|
|
||||||
sh 'dotnet build'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Middleware;
|
|
||||||
|
|
||||||
using DigitalData.Core.Exceptions;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Middleware for handling exceptions globally in the application.
|
|
||||||
/// Captures exceptions thrown during the request pipeline execution,
|
|
||||||
/// logs them, and returns an appropriate HTTP response with a JSON error message.
|
|
||||||
/// </summary>
|
|
||||||
public class ExceptionHandlingMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="next">The next middleware in the request pipeline.</param>
|
|
||||||
/// <param name="logger">The logger instance for logging exceptions.</param>
|
|
||||||
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invokes the middleware to handle the HTTP request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The HTTP context of the current request.</param>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _next(context); // Continue down the pipeline
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await HandleExceptionAsync(context, ex, _logger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles exceptions by logging them and writing an appropriate JSON response.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The HTTP context of the current request.</param>
|
|
||||||
/// <param name="exception">The exception that occurred.</param>
|
|
||||||
/// <param name="logger">The logger instance for logging the exception.</param>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
|
|
||||||
{
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
|
|
||||||
string message;
|
|
||||||
|
|
||||||
switch (exception)
|
|
||||||
{
|
|
||||||
case BadRequestException badRequestEx:
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
|
||||||
message = badRequestEx.Message;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotFoundException notFoundEx:
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
|
||||||
message = notFoundEx.Message;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.LogError(exception, "Unhandled exception occurred.");
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
|
||||||
message = "An unexpected error occurred.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.Response.WriteAsync(JsonSerializer.Serialize(new
|
|
||||||
{
|
|
||||||
message
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,28 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the keys and default values used for authentication token handling
|
|
||||||
/// within the Envelope Generator API.
|
|
||||||
/// </summary>
|
|
||||||
public class AuthTokenKeys
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name of the cookie used to store the authentication token.
|
|
||||||
/// </summary>
|
|
||||||
public string Cookie { get; init; } = "AuthToken";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name of the query string parameter used to pass the authentication token.
|
|
||||||
/// </summary>
|
|
||||||
public string QueryString { get; init; } = "AuthToken";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the expected issuer value for the authentication token.
|
|
||||||
/// </summary>
|
|
||||||
public string Issuer { get; init; } = "auth.digitaldata.works";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the expected audience value for the authentication token.
|
|
||||||
/// </summary>
|
|
||||||
public string Audience { get; init; } = "sign-flow.digitaldata.works";
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ConnectionString
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string Value { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -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,13 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Password">Das erforderliche Passwort für das Login.</param>
|
|
||||||
/// <param name="UserId">Die optionale ID des Benutzers.</param>
|
|
||||||
/// <param name="Username">Der optionale Benutzername.</param>
|
|
||||||
public record Login([Required] string Password, int? UserId = null, string? Username = null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -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,18 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration options for distributed caching.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CacheOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration section name in appsettings.json.
|
|
||||||
/// </summary>
|
|
||||||
public const string SectionName = "Cache";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Signature cache expiration time.
|
|
||||||
/// If null, signatures will not expire automatically.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan? SignatureCacheExpiration { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
using DigitalData.Core.API;
|
|
||||||
using DigitalData.Core.Application;
|
|
||||||
using EnvelopeGenerator.Infrastructure;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Localization;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Globalization;
|
|
||||||
using Scalar.AspNetCore;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using DigitalData.UserManager.DependencyInjection;
|
|
||||||
using EnvelopeGenerator.Application;
|
|
||||||
using DigitalData.Auth.Client;
|
|
||||||
using DigitalData.Core.Abstractions;
|
|
||||||
using EnvelopeGenerator.API.Models;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
|
||||||
using EnvelopeGenerator.API.Middleware;
|
|
||||||
using EnvelopeGenerator.API.Options;
|
|
||||||
using NLog.Web;
|
|
||||||
using NLog;
|
|
||||||
using DigitalData.Auth.Claims;
|
|
||||||
using EnvelopeGenerator.API;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
|
||||||
logger.Info("Logging initialized!");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true);
|
|
||||||
|
|
||||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
|
||||||
|
|
||||||
if (!builder.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
builder.Logging.ClearProviders();
|
|
||||||
builder.Host.UseNLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = builder.Configuration;
|
|
||||||
|
|
||||||
var deferredProvider = new DeferredServiceProvider();
|
|
||||||
|
|
||||||
builder.Services.AddControllers()
|
|
||||||
.AddJsonOptions(options =>
|
|
||||||
{
|
|
||||||
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
|
||||||
});
|
|
||||||
builder.Services.AddHttpClient();
|
|
||||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
|
||||||
|
|
||||||
// CORS Policy
|
|
||||||
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
|
||||||
throw new InvalidOperationException("AllowedOrigins section is missing in the configuration.");
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowSpecificOriginsPolicy", builder =>
|
|
||||||
{
|
|
||||||
builder.WithOrigins(allowedOrigins)
|
|
||||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowCredentials();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Swagger
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen(options =>
|
|
||||||
{
|
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
|
||||||
{
|
|
||||||
Version = "v1",
|
|
||||||
Title = "signFLOW Absender-API",
|
|
||||||
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschlägen in der signFLOW-Anwendung.",
|
|
||||||
Contact = new OpenApiContact
|
|
||||||
{
|
|
||||||
Name = "Digital Data GmbH",
|
|
||||||
Url = new Uri("https://digitaldata.works/digitale-signatur#kontakt"),
|
|
||||||
Email = "info-flow@digitaldata.works"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
Name = "Authorization",
|
|
||||||
Type = SecuritySchemeType.Http,
|
|
||||||
Scheme = "bearer",
|
|
||||||
BearerFormat = "JWT",
|
|
||||||
In = ParameterLocation.Header,
|
|
||||||
Description = "JWT-Autorisierungs-Header unter Verwendung des Bearer-Schemas.",
|
|
||||||
});
|
|
||||||
|
|
||||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
||||||
{
|
|
||||||
{
|
|
||||||
new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
Reference = new OpenApiReference
|
|
||||||
{
|
|
||||||
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
|
||||||
Id = "Bearer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Array.Empty<string>()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
|
|
||||||
foreach (var xmlFile in xmlFiles)
|
|
||||||
{
|
|
||||||
options.IncludeXmlComments(xmlFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
options.DocumentFilter<EnvelopeGenerator.API.Documentation.AuthProxyDocumentFilter>();
|
|
||||||
});
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
builder.Services.AddOpenApi();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//Add EF Core dbcontext
|
|
||||||
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
|
|
||||||
var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default";
|
|
||||||
var connStr = config.GetConnectionString(cnnStrName)
|
|
||||||
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
|
|
||||||
|
|
||||||
builder.Services.Configure<ConnectionString>(cs => cs.Value = connStr);
|
|
||||||
|
|
||||||
builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
|
|
||||||
|
|
||||||
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"));
|
|
||||||
|
|
||||||
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
|
|
||||||
|
|
||||||
builder.Services.AddAuthentication(options =>
|
|
||||||
{
|
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
})
|
|
||||||
.AddJwtBearer(AuthScheme.Sender, opt =>
|
|
||||||
{
|
|
||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
|
||||||
{
|
|
||||||
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
|
||||||
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
|
||||||
return [publicKey.SecurityKey];
|
|
||||||
},
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidIssuer = authTokenKeys.Issuer,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidAudience = authTokenKeys.Audience,
|
|
||||||
};
|
|
||||||
|
|
||||||
opt.Events = new JwtBearerEvents
|
|
||||||
{
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
// if there is no token read related cookie or query string
|
|
||||||
if (context.Token is null) // if there is no token
|
|
||||||
{
|
|
||||||
if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null)
|
|
||||||
context.Token = cookieToken;
|
|
||||||
else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken))
|
|
||||||
context.Token = queryStrToken;
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
// Per-envelope receiver scheme: reads the JWT from the cookie named
|
|
||||||
// AuthTokenSignFLOWReceiver.{envelope_key} where envelope_key is the
|
|
||||||
// last path segment of the request URL.
|
|
||||||
// This enables simultaneous authentication for multiple envelopes
|
|
||||||
// within the same browser session.
|
|
||||||
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
|
||||||
{
|
|
||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
|
||||||
{
|
|
||||||
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
|
||||||
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
|
||||||
return [publicKey.SecurityKey];
|
|
||||||
},
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidIssuer = authTokenKeys.Issuer,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidAudience = authTokenKeys.Audience,
|
|
||||||
};
|
|
||||||
|
|
||||||
opt.Events = new JwtBearerEvents
|
|
||||||
{
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
// Derive the envelope key from the last route segment: /{envelope_key}
|
|
||||||
var envelopeKey = paths?.LastOrDefault();
|
|
||||||
|
|
||||||
if (envelopeKey is not null)
|
|
||||||
{
|
|
||||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
|
||||||
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null)
|
|
||||||
context.Token = cookieToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
},
|
|
||||||
OnTokenValidated = context =>
|
|
||||||
{
|
|
||||||
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var envelopeKey = paths?.LastOrDefault();
|
|
||||||
|
|
||||||
var sub = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
|
||||||
?? context.Principal?.FindFirst("sub")?.Value;
|
|
||||||
|
|
||||||
if (envelopeKey is null || sub != envelopeKey)
|
|
||||||
context.Fail("Envelope key in the path does not match the token subject.");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Authentication
|
|
||||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
||||||
.AddCookie(options =>
|
|
||||||
{
|
|
||||||
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
|
|
||||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
|
|
||||||
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
|
|
||||||
options.LoginPath = "/api/auth/login";
|
|
||||||
options.LogoutPath = "/api/auth/logout";
|
|
||||||
options.SlidingExpiration = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddAuthorizationBuilder()
|
|
||||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
|
|
||||||
.RequireRole(Role.Sender, Role.Receiver.Full)
|
|
||||||
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
|
||||||
.AddPolicy(AuthPolicy.Sender, policy => policy
|
|
||||||
.RequireRole(Role.Sender)
|
|
||||||
.AddAuthenticationSchemes(AuthScheme.Sender))
|
|
||||||
.AddPolicy(AuthPolicy.Receiver, policy => policy
|
|
||||||
.AddAuthenticationSchemes(AuthScheme.Receiver)
|
|
||||||
.RequireAuthenticatedUser()
|
|
||||||
.RequireRole(Role.Receiver.Full, "receiver"))
|
|
||||||
.AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA));
|
|
||||||
|
|
||||||
// User manager
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
builder.Services.AddUserManager<EGDbContext>();
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
// LDAP
|
|
||||||
builder.ConfigureBySection<DirectorySearchOptions>();
|
|
||||||
builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions"));
|
|
||||||
|
|
||||||
// Localizer
|
|
||||||
builder.Services.AddCookieBasedLocalizer();
|
|
||||||
|
|
||||||
// Cache options
|
|
||||||
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
|
|
||||||
|
|
||||||
// Distributed Cache - SQL Server
|
|
||||||
builder.Services.AddDistributedSqlServerCache(options =>
|
|
||||||
{
|
|
||||||
config.GetSection("Cache:SqlServer").Bind(options);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(options.ConnectionString))
|
|
||||||
{
|
|
||||||
options.ConnectionString = connStr;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Envelope generator serives
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
builder.Services
|
|
||||||
.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
|
||||||
{
|
|
||||||
opt.AddDbTriggerParams(config);
|
|
||||||
opt.AddDbContext((provider, options) =>
|
|
||||||
{
|
|
||||||
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
|
|
||||||
options.UseSqlServer(connStr)
|
|
||||||
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
|
|
||||||
.EnableSensitiveDataLogging()
|
|
||||||
.EnableDetailedErrors();
|
|
||||||
});
|
|
||||||
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
|
|
||||||
})
|
|
||||||
.AddEnvelopeGeneratorServices(config);
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
deferredProvider.Factory = () => app.Services;
|
|
||||||
|
|
||||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
|
||||||
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
app.MapOpenApi();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment() || (app.IsDevOrDiP() && config.GetValue<bool>("UseSwagger")))
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
app.MapScalarApiReference();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set CORS policy
|
|
||||||
app.UseCors("AllowSpecificOriginsPolicy");
|
|
||||||
|
|
||||||
// Localizer
|
|
||||||
string[] supportedCultureNames = ["de-DE", "en-US"];
|
|
||||||
IList<CultureInfo> list = [.. supportedCultureNames.Select(cn => new CultureInfo(cn))];
|
|
||||||
var cultureInfo = list.FirstOrDefault() ?? throw new InvalidOperationException("There is no supported culture.");
|
|
||||||
var requestLocalizationOptions = new RequestLocalizationOptions
|
|
||||||
{
|
|
||||||
SupportedCultures = list,
|
|
||||||
SupportedUICultures = list
|
|
||||||
};
|
|
||||||
requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
|
|
||||||
app.UseRequestLocalization(requestLocalizationOptions);
|
|
||||||
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
// Catch-all YARP proxy — only forward requests that are not swagger/scalar/openapi paths.
|
|
||||||
app.MapReverseProxy();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.Error(ex, "Stopped program because of exception");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
|
||||||
-->
|
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<WebPublishMethod>Package</WebPublishMethod>
|
|
||||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
|
||||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
|
||||||
<SiteUrlToLaunchAfterPublish />
|
|
||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
|
||||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
|
||||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
|
||||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
|
||||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
|
||||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:4176",
|
|
||||||
"sslPort": 44300
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "http://localhost:5131",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "sender",
|
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"angular": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "",
|
|
||||||
"applicationUrl": "https://localhost:7174;http://localhost:5131",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,256 +0,0 @@
|
|||||||
{
|
|
||||||
"UseSwagger": true,
|
|
||||||
"UseDbMigration": false,
|
|
||||||
"DiPMode": true,
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"AllowedOrigins": [ "http://localhost:4200", "http://172.24.12.39:9090", "https://localhost:8088", "http://localhost:5131", "http://localhost:7192" ],
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
|
|
||||||
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
|
||||||
},
|
|
||||||
"DirectorySearchOptions": {
|
|
||||||
"ServerName": "DD-VMP01-DC01",
|
|
||||||
"Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works",
|
|
||||||
"UserCacheExpirationDays": 1,
|
|
||||||
"CustomSearchFilters": {
|
|
||||||
"User": "(&(objectClass=user)(sAMAccountName=*))",
|
|
||||||
"Group": "(&(objectClass=group)(samAccountName=*))"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AuthClientParams": {
|
|
||||||
"Url": "http://172.24.12.39:9090/auth-hub",
|
|
||||||
"PublicKeys": [
|
|
||||||
{
|
|
||||||
"Issuer": "auth.digitaldata.works",
|
|
||||||
"Audience": "sign-flow.digitaldata.works"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"RetryDelay": "00:00:05"
|
|
||||||
},
|
|
||||||
"AuthTokenKeys": {
|
|
||||||
"Cookie": "AuthToken",
|
|
||||||
"QueryString": "AuthToken",
|
|
||||||
"Issuer": "auth.digitaldata.works",
|
|
||||||
"Audience": "sign-flow.digitaldata.works"
|
|
||||||
},
|
|
||||||
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
|
|
||||||
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.
|
|
||||||
"default-src 'self'",
|
|
||||||
"script-src 'self' 'nonce-{0}' 'unsafe-eval'",
|
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com:*",
|
|
||||||
"img-src 'self' data: https: blob:",
|
|
||||||
"font-src 'self' https://fonts.gstatic.com:*",
|
|
||||||
"connect-src 'self' https://nominatim.openstreetmap.org:* http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* blob:",
|
|
||||||
"frame-src 'self'",
|
|
||||||
"media-src 'self'",
|
|
||||||
"object-src 'self'"
|
|
||||||
],
|
|
||||||
"NLog": {
|
|
||||||
"throwConfigExceptions": true,
|
|
||||||
"variables": {
|
|
||||||
"logDirectory": "E:\\LogFiles\\Digital Data\\signFlow",
|
|
||||||
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Web"
|
|
||||||
},
|
|
||||||
"targets": {
|
|
||||||
"infoLogs": {
|
|
||||||
"type": "File",
|
|
||||||
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
|
|
||||||
"maxArchiveDays": 30
|
|
||||||
},
|
|
||||||
"errorLogs": {
|
|
||||||
"type": "File",
|
|
||||||
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
|
|
||||||
"maxArchiveDays": 30
|
|
||||||
},
|
|
||||||
"criticalLogs": {
|
|
||||||
"type": "File",
|
|
||||||
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
|
|
||||||
"maxArchiveDays": 30
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Trace, Debug, Info, Warn, Error and *Fatal*
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"logger": "*",
|
|
||||||
"minLevel": "Info",
|
|
||||||
"maxLevel": "Warn",
|
|
||||||
"writeTo": "infoLogs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"logger": "*",
|
|
||||||
"level": "Error",
|
|
||||||
"writeTo": "errorLogs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"logger": "*",
|
|
||||||
"level": "Fatal",
|
|
||||||
"writeTo": "criticalLogs"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ContactLink": {
|
|
||||||
"Label": "Kontakt",
|
|
||||||
"Href": "https://digitaldata.works/",
|
|
||||||
"HrefLang": "de",
|
|
||||||
"Target": "_blank",
|
|
||||||
"Title": "Digital Data GmbH"
|
|
||||||
},
|
|
||||||
/* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx).
|
|
||||||
To add a new language, first you should write the required resx file.
|
|
||||||
first is the default culture name. */
|
|
||||||
"Cultures": [
|
|
||||||
{
|
|
||||||
"Language": "de-DE",
|
|
||||||
"FIClass": "fi-de"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Language": "en-US",
|
|
||||||
"FIClass": "fi-us"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"DisableMultiLanguage": false,
|
|
||||||
"Regexes": [
|
|
||||||
{
|
|
||||||
"Pattern": "/^\\p{L}+(?:([\\ \\-\\']|(\\.\\ ))\\p{L}+)*$/u",
|
|
||||||
"Name": "City",
|
|
||||||
"Platforms": [ ".NET" ]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Pattern": "/^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$/",
|
|
||||||
"Name": "City",
|
|
||||||
"Platforms": [ "javascript" ]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"CustomImages": {
|
|
||||||
"App": {
|
|
||||||
"Src": "/img/DD_signFLOW_LOGO.png",
|
|
||||||
"Classes": {
|
|
||||||
"Main": "signFlow-logo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Company": {
|
|
||||||
"Src": "/img/digital_data.svg",
|
|
||||||
"Classes": {
|
|
||||||
"Show": "dd-show-logo",
|
|
||||||
"Locked": "dd-locked-logo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DispatcherParams": {
|
|
||||||
"SendingProfile": 1,
|
|
||||||
"AddedWho": "DDEnvelopGenerator",
|
|
||||||
"ReminderTypeId": 202377,
|
|
||||||
"EmailAttmt1": ""
|
|
||||||
},
|
|
||||||
"MailParams": {
|
|
||||||
"Placeholders": {
|
|
||||||
"[NAME_PORTAL]": "signFlow",
|
|
||||||
"[SIGNATURE_TYPE]": "signieren",
|
|
||||||
"[REASON]": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"GtxMessagingParams": {
|
|
||||||
"Uri": "https://rest.gtx-messaging.net",
|
|
||||||
"Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json",
|
|
||||||
"Headers": {},
|
|
||||||
"QueryParams": {
|
|
||||||
"from": "signFlow"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TFARegParams": {
|
|
||||||
"TimeLimit": "00:30:00"
|
|
||||||
},
|
|
||||||
"DbTriggerParams": {
|
|
||||||
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
|
||||||
"EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
|
||||||
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
|
||||||
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
|
||||||
"Receiver": [],
|
|
||||||
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
|
||||||
},
|
|
||||||
"Cache": {
|
|
||||||
"SignatureCacheExpiration": null,
|
|
||||||
"SqlServer": {
|
|
||||||
"ConnectionString": null,
|
|
||||||
"SchemaName": "dbo",
|
|
||||||
"TableName": "TBDD_CACHE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MainPageTitle": null,
|
|
||||||
"AnnotationParams": {
|
|
||||||
"Background": {
|
|
||||||
"Margin": 0.20,
|
|
||||||
"BackgroundColor": {
|
|
||||||
"R": 222,
|
|
||||||
"G": 220,
|
|
||||||
"B": 215
|
|
||||||
},
|
|
||||||
"BorderColor": {
|
|
||||||
"R": 204,
|
|
||||||
"G": 202,
|
|
||||||
"B": 198
|
|
||||||
},
|
|
||||||
"BorderStyle": "underline",
|
|
||||||
"BorderWidth": 4
|
|
||||||
},
|
|
||||||
"DefaultAnnotation": {
|
|
||||||
"Width": 1,
|
|
||||||
"Height": 0.5,
|
|
||||||
"MarginTop": 1
|
|
||||||
},
|
|
||||||
"Annotations": [
|
|
||||||
{
|
|
||||||
"Name": "Signature",
|
|
||||||
"MarginTop": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "PositionLabel",
|
|
||||||
"VerBoundAnnotName": "Signature",
|
|
||||||
"WidthRatio": 1.2,
|
|
||||||
"HeightRatio": 0.5,
|
|
||||||
"MarginTopRatio": 0.22
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "Position",
|
|
||||||
"VerBoundAnnotName": "PositionLabel",
|
|
||||||
"WidthRatio": 1.2,
|
|
||||||
"HeightRatio": 0.5,
|
|
||||||
"MarginTopRatio": -0.05
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "CityLabel",
|
|
||||||
"VerBoundAnnotName": "Position",
|
|
||||||
"WidthRatio": 1.2,
|
|
||||||
"HeightRatio": 0.5,
|
|
||||||
"MarginTopRatio": 0.05
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "City",
|
|
||||||
"VerBoundAnnotName": "CityLabel",
|
|
||||||
"WidthRatio": 1.2,
|
|
||||||
"HeightRatio": 0.5,
|
|
||||||
"MarginTopRatio": -0.05
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "DateLabel",
|
|
||||||
"VerBoundAnnotName": "City",
|
|
||||||
"WidthRatio": 1.55,
|
|
||||||
"HeightRatio": 0.5,
|
|
||||||
"MarginTopRatio": 0.05
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "Date",
|
|
||||||
"VerBoundAnnotName": "DateLabel",
|
|
||||||
"WidthRatio": 1.55,
|
|
||||||
"HeightRatio": 0.5,
|
|
||||||
"MarginTopRatio": -0.1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
{
|
|
||||||
"ReverseProxy": {
|
|
||||||
"Routes": {
|
|
||||||
"receiver-ui-root": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 300,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathSet": "/index.html" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-sender": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/sender/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathSet": "/index.html" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-envelope": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/envelope/{EnvelopeKey}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathSet": "/index.html" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-envelope-dxreportviewer": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 90,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/envelope/{EnvelopeKey}/DxReportViewer",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathSet": "/index.html" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-blazor-framework": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/_framework/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-blazor-content": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/_content/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-static-css": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/css/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{
|
|
||||||
"ResponseHeader": "Cache-Control",
|
|
||||||
"Set": "no-cache, no-store, must-revalidate",
|
|
||||||
"When": "Always"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-static-js": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/js/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{
|
|
||||||
"ResponseHeader": "Cache-Control",
|
|
||||||
"Set": "no-cache, no-store, must-revalidate",
|
|
||||||
"When": "Always"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-fake-data": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/fake-data/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-appsettings": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/appsettings.json",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-appsettings-dev": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/appsettings.Development.json",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-styles": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/EnvelopeGenerator.ReceiverUI.styles.css",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-fonts": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/fonts/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-images": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/images/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth-login": {
|
|
||||||
"ClusterId": "auth-hub",
|
|
||||||
"Match": {
|
|
||||||
"Path": "/api/auth",
|
|
||||||
"Methods": [ "POST" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathSet": "/api/auth/sign-flow" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"auth-envelope-receiver-login": {
|
|
||||||
"ClusterId": "auth-hub",
|
|
||||||
"Match": {
|
|
||||||
"Path": "/api/Auth/envelope-receiver/{key}",
|
|
||||||
"Methods": [ "POST" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
|
|
||||||
{
|
|
||||||
"QueryValueParameter": "cookie",
|
|
||||||
"Set": "true"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Clusters": {
|
|
||||||
"receiver-ui": {
|
|
||||||
"Destinations": {
|
|
||||||
"primary": {
|
|
||||||
"Address": "https://localhost:52936"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth-hub": {
|
|
||||||
"Destinations": {
|
|
||||||
"primary": {
|
|
||||||
"Address": "http://172.24.12.39:9090"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Behaviors;
|
||||||
|
|
||||||
|
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||||
|
where TRequest : notnull
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||||
|
|
||||||
|
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
||||||
|
{
|
||||||
|
_validators = validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> Handle(
|
||||||
|
TRequest request,
|
||||||
|
RequestHandlerDelegate<TResponse> next,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_validators.Any())
|
||||||
|
{
|
||||||
|
var context = new ValidationContext<TRequest>(request);
|
||||||
|
var failures = _validators
|
||||||
|
.Select(v => v.Validate(context))
|
||||||
|
.SelectMany(r => r.Errors)
|
||||||
|
.Where(f => f is not null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (failures.Count > 0)
|
||||||
|
{
|
||||||
|
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
|
||||||
|
throw new BadRequestException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object representing document metadata and elements without binary payload.
|
||||||
|
/// </summary>
|
||||||
|
public record DocumentDetailsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the document.
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the envelope ID to which the document belongs.
|
||||||
|
/// </summary>
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the date and time when the document was added.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
|
||||||
|
}
|
||||||
@@ -3,30 +3,25 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocumentDto
|
public record DocumentDto : DocumentDetailsDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the document.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public DocumentDto() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the envelope ID to which the document belongs.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EnvelopeId { get; set; }
|
/// <param name="byteData"></param>
|
||||||
|
/// <param name="details"></param>
|
||||||
/// <summary>
|
public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
|
||||||
/// Gets or sets the date and time when the document was added.
|
{
|
||||||
/// </summary>
|
ByteData = byteData;
|
||||||
public DateTime AddedWhen { get; set; }
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the binary data of the document, if available.
|
/// Gets or sets the binary data of the document, if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? ByteData { get; set; }
|
public byte[]? ByteData { get; init; }
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -86,6 +86,41 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWhenDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -135,4 +170,4 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<HistoryDto>? Histories { get; set; }
|
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ public record EnvelopeReceiverDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PrivateMessage { get; init; }
|
public string? PrivateMessage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? AccessCode { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -83,4 +88,4 @@ public record EnvelopeReceiverDto
|
|||||||
h.Receiver?.Id == ReceiverId &&
|
h.Receiver?.Id == ReceiverId &&
|
||||||
h.Status == EnvelopeStatus.DocumentSigned
|
h.Status == EnvelopeStatus.DocumentSigned
|
||||||
) ?? false;
|
) ?? false;
|
||||||
}
|
}
|
||||||
|
|||||||
16
EnvelopeGenerator.Application/Common/Dto/PdfPageImageDto.cs
Normal file
16
EnvelopeGenerator.Application/Common/Dto/PdfPageImageDto.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
public sealed class PdfPageImageDto
|
||||||
|
{
|
||||||
|
public int PageNumber { get; init; }
|
||||||
|
|
||||||
|
public double WidthPt { get; init; }
|
||||||
|
|
||||||
|
public double HeightPt { get; init; }
|
||||||
|
|
||||||
|
public int WidthPx { get; init; }
|
||||||
|
|
||||||
|
public int HeightPx { get; init; }
|
||||||
|
|
||||||
|
public string ImageDataUrl { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
public sealed class PdfRenderRequestDto
|
||||||
|
{
|
||||||
|
public byte[]? PdfBytes { get; init; }
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ public sealed record Signature
|
|||||||
public string? Position
|
public string? Position
|
||||||
{
|
{
|
||||||
get => _position;
|
get => _position;
|
||||||
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,4 +62,4 @@ public sealed record Signature
|
|||||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Place { get; init; }
|
public required string Place { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
|
||||||
|
public interface IPdfPageImageRenderer
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ public class MappingProfile : Profile
|
|||||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
|
CreateMap<Document, DocumentDetailsDto>();
|
||||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
@@ -68,4 +69,4 @@ public class MappingProfile : Profile
|
|||||||
? new SmsResponse() { Ok = true }
|
? new SmsResponse() { Ok = true }
|
||||||
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
|
||||||
|
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||||
|
{
|
||||||
|
public required Envelope Envelope { get; init; }
|
||||||
|
|
||||||
|
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||||
|
|
||||||
|
public required string Reason { get; init; }
|
||||||
|
|
||||||
|
public required string SenderReference { get; init; }
|
||||||
|
|
||||||
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||||
|
|
||||||
|
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||||
|
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||||
|
|
||||||
|
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||||
|
{
|
||||||
|
public SendDeletedMailHandler(
|
||||||
|
IRepository<EmailTemplate> tempRepo,
|
||||||
|
IRepository<EmailOut> emailOutRepo,
|
||||||
|
IOptions<MailParams> mailParamsOptions,
|
||||||
|
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||||
|
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||||
|
{
|
||||||
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
|
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||||
|
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||||
|
{
|
||||||
|
var senderName = notification.Envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
|
senderName = notification.SenderReference;
|
||||||
|
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||||
|
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||||
|
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||||
|
["[REASON]"] = notification.Reason
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ using System.Reflection;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
using EnvelopeGenerator.Application.Common.Behaviors;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application;
|
namespace EnvelopeGenerator.Application;
|
||||||
|
|
||||||
@@ -59,24 +61,32 @@ public static class DependencyInjection
|
|||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
|
// Generic request validation
|
||||||
|
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
||||||
|
|
||||||
// Register SignCommand pipeline behaviors in execution order
|
// Register SignCommand pipeline behaviors in execution order
|
||||||
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
|
|
||||||
// 1. AnnotationBehavior - Saves annotations (executes second)
|
// 1. AnnotationBehavior - Saves annotations (executes second)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
|
||||||
|
|
||||||
|
// 2. AnnotationBehavior - Saves annotations for legacy web flow
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||||
|
|
||||||
// 2. DocStatusBehavior - Creates document status (executes third)
|
// 3. DocStatusBehavior - Creates document status
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||||
|
|
||||||
// 3. HistoryBehavior - Records history (executes fourth)
|
// 4. HistoryBehavior - Records history
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||||
|
|
||||||
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
|
|||||||
// If EnvelopeReceiver is not provided, query it from database
|
// If EnvelopeReceiver is not provided, query it from database
|
||||||
if (request.EnvelopeReceiver is null)
|
if (request.EnvelopeReceiver is null)
|
||||||
{
|
{
|
||||||
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
var er = await _erRepo.Query
|
||||||
|
.Where(request, notnull: true)
|
||||||
|
.Include(x => x.Envelope)
|
||||||
|
.Include(x => x.Receiver)
|
||||||
|
.SingleOrDefaultAsync(cancellationToken)
|
||||||
?? throw new NotFoundException("EnvelopeReceiver not found");
|
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||||
|
|
||||||
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|||||||
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||||
|
|
||||||
var elements = await _elementRepo
|
var elements = await _elementRepo
|
||||||
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
||||||
.Where(e => e.ReceiverId == request.Receiver.Id)
|
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
foreach (var element in elements)
|
foreach (var element in elements)
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a query to read document metadata/details without binary payload.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
|
||||||
|
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||||
|
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles queries for reading document details without loading BYTE_DATA.
|
||||||
|
/// </summary>
|
||||||
|
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Document> _repo;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var docQuery = _repo.Query.AsNoTracking();
|
||||||
|
|
||||||
|
if (query.UserId is int userId)
|
||||||
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
|
return await docQuery
|
||||||
|
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
||||||
|
.ProjectTo<DocumentDetailsDto>(_mapper.ConfigurationProvider)
|
||||||
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
|
||||||
|
{
|
||||||
|
public ReadDocumentDetailsQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EnvelopeId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("EnvelopeId is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
|
||||||
|
{
|
||||||
|
public ReadDocumentQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
|
||||||
|
.WithMessage("Id or EnvelopeId is required.");
|
||||||
|
|
||||||
|
RuleFor(x => x.Id)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.When(x => x.Id.HasValue)
|
||||||
|
.WithMessage("Id must be greater than 0.");
|
||||||
|
|
||||||
|
RuleFor(x => x.EnvelopeId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.When(x => x.EnvelopeId.HasValue)
|
||||||
|
.WithMessage("EnvelopeId must be greater than 0.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
||||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
|
|||||||
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
||||||
{
|
{
|
||||||
private readonly IRepository<EnvelopeType> _repository;
|
private readonly IRepository<EnvelopeType> _repository;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="repository"></param>
|
/// <param name="repository"></param>
|
||||||
/// <param name="mapper"></param>
|
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
|
||||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
|
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_mapper = mapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var types = await _repository.Query.ToListAsync(cancellationToken);
|
// TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
|
||||||
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
|
// Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
|
||||||
|
return await _repository.Query
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(t => new EnvelopeTypeDto
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
Title = t.Title ?? string.Empty,
|
||||||
|
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
||||||
|
ExpiresDays = t.ExpiresDays,
|
||||||
|
CertificationType = (int?)t.CertificationType,
|
||||||
|
UseAccessCode = t.UseAccessCode,
|
||||||
|
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
||||||
|
AddedWhen = t.AddedWhen,
|
||||||
|
ChangedWhen = t.ChangedWhen,
|
||||||
|
ExpiresWarningDays = t.ExpiresWarningDays,
|
||||||
|
SendReminderEmails = t.SendReminderEmails,
|
||||||
|
FirstReminderDays = t.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = t.ReminderIntervalDays,
|
||||||
|
ContractType = t.ContractType
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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.Common.Interfaces.SQLExecutor;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
@@ -13,23 +12,96 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optionale Umschlag-ID fuer Aktualisierungsvorgaenge.
|
||||||
|
/// </summary>
|
||||||
|
public int? EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kennung des ausgewaehlten Umschlagtyps (Vorlage).
|
||||||
|
/// </summary>
|
||||||
|
public int? EnvelopeTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sprachcode fuer Umschlaginhalt und Benachrichtigungen.
|
||||||
|
/// </summary>
|
||||||
|
public string? Language { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob der Zugriffscode fuer Empfaenger aktiviert ist.
|
||||||
|
/// </summary>
|
||||||
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zertifizierungstyp fuer den Signaturprozess.
|
||||||
|
/// </summary>
|
||||||
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finale E-Mail-Einstellung fuer die Empfaenger.
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob Erinnerungs-E-Mails aktiviert sind.
|
||||||
|
/// </summary>
|
||||||
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage bis zur ersten Erinnerung.
|
||||||
|
/// </summary>
|
||||||
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intervall in Tagen zwischen Erinnerungs-E-Mails.
|
||||||
|
/// </summary>
|
||||||
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage bis zum Ablauf des Umschlags.
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWhenDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage vor Ablauf, ab denen gewarnt wird.
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
public string? Title { get; set; }
|
||||||
public required string Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
public string? Message { get; set; }
|
||||||
public required string Message { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TFAEnabled { get; set; } = false;
|
public bool TFAEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob der Umschlag nach dem Speichern sofort versendet werden soll.
|
||||||
|
/// </summary>
|
||||||
|
public bool Send { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optionaler PDF-Dokumentinhalt als Base64.
|
||||||
|
/// </summary>
|
||||||
|
public CreateEnvelopeDocumentDto? Document { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empfaengerliste inklusive optionaler Signaturfeld-Koordinaten.
|
||||||
|
/// </summary>
|
||||||
|
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID des Absenders
|
/// ID des Absenders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -55,4 +127,14 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool UseSQLExecutor { get; set; } = true;
|
public bool UseSQLExecutor { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
||||||
|
|
||||||
|
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
|
||||||
|
|
||||||
|
public record CreateEnvelopeReceiverDto(
|
||||||
|
string EmailAddress,
|
||||||
|
string? Name,
|
||||||
|
string? PhoneNumber,
|
||||||
|
List<CreateEnvelopeFieldDto> Fields);
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
@@ -19,6 +29,28 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
|
|
||||||
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
||||||
|
|
||||||
|
private IRepository<Document> DocumentRepository => _provider.GetRequiredService<IRepository<Document>>();
|
||||||
|
|
||||||
|
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => _provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
||||||
|
|
||||||
|
private IRepository<EnvelopeType> EnvelopeTypeRepository => _provider.GetRequiredService<IRepository<EnvelopeType>>();
|
||||||
|
|
||||||
|
private IRepository<Receiver> ReceiverRepository => _provider.GetRequiredService<IRepository<Receiver>>();
|
||||||
|
|
||||||
|
private IRepository<DocReceiverElement> DocReceiverElementRepository => _provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
||||||
|
|
||||||
|
private IRepository<EmailTemplate> EmailTemplateRepository => _provider.GetRequiredService<IRepository<EmailTemplate>>();
|
||||||
|
|
||||||
|
private IRepository<EmailOut> EmailOutRepository => _provider.GetRequiredService<IRepository<EmailOut>>();
|
||||||
|
|
||||||
|
private MailParams MailParams => _provider.GetRequiredService<IOptions<MailParams>>().Value;
|
||||||
|
|
||||||
|
private DispatcherParams DispatcherParams => _provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
||||||
|
|
||||||
|
private IConfigService ConfigService => _provider.GetRequiredService<IConfigService>();
|
||||||
|
|
||||||
|
private IMediator Mediator => _provider.GetRequiredService<IMediator>();
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,10 +72,572 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var envelope = request.UseSQLExecutor
|
var now = DateTime.Now;
|
||||||
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel)
|
var title = request.Title?.Trim() ?? string.Empty;
|
||||||
: await Repository.CreateAsync(request, cancel);
|
var message = request.Message?.Trim() ?? string.Empty;
|
||||||
|
var settings = await ResolveEnvelopeSettingsAsync(request, existing: null, cancel);
|
||||||
|
|
||||||
|
Envelope envelope;
|
||||||
|
if (request.EnvelopeId is int envelopeId)
|
||||||
|
{
|
||||||
|
envelope = await Repository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == envelopeId, cancel)
|
||||||
|
?? throw new BadRequestException($"Envelope {envelopeId} not found.");
|
||||||
|
|
||||||
|
if (envelope.UserId != request.UserId)
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
settings = await ResolveEnvelopeSettingsAsync(request, envelope, cancel);
|
||||||
|
|
||||||
|
await Repository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Title = title;
|
||||||
|
e.Message = message;
|
||||||
|
e.TfaEnabled = request.TFAEnabled;
|
||||||
|
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
e.Language = settings.Language;
|
||||||
|
e.UseAccessCode = settings.UseAccessCode;
|
||||||
|
e.CertificationType = settings.CertificationType;
|
||||||
|
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
e.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
e.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
e.ContractType = settings.ContractType;
|
||||||
|
e.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
e.ChangedWhen = now;
|
||||||
|
}, e => e.Id == envelopeId, cancel);
|
||||||
|
|
||||||
|
envelope.Title = title;
|
||||||
|
envelope.Message = message;
|
||||||
|
envelope.TfaEnabled = request.TFAEnabled;
|
||||||
|
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
envelope.Language = settings.Language;
|
||||||
|
envelope.UseAccessCode = settings.UseAccessCode;
|
||||||
|
envelope.CertificationType = settings.CertificationType;
|
||||||
|
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
envelope.ContractType = settings.ContractType;
|
||||||
|
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
envelope.ChangedWhen = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
envelope = request.UseSQLExecutor
|
||||||
|
? await Executor.CreateEnvelopeAsync(request.UserId, title, message, request.TFAEnabled, cancel)
|
||||||
|
: await Repository.CreateAsync(new Envelope
|
||||||
|
{
|
||||||
|
UserId = request.UserId,
|
||||||
|
Status = EnvelopeStatus.EnvelopeCreated,
|
||||||
|
Uuid = Guid.NewGuid().ToString(),
|
||||||
|
Title = title,
|
||||||
|
Message = message,
|
||||||
|
AddedWhen = now,
|
||||||
|
ChangedWhen = now,
|
||||||
|
Comment = string.Empty,
|
||||||
|
EnvelopeTypeId = settings.EnvelopeTypeId,
|
||||||
|
Language = settings.Language,
|
||||||
|
TfaEnabled = request.TFAEnabled,
|
||||||
|
UseAccessCode = settings.UseAccessCode,
|
||||||
|
CertificationType = settings.CertificationType,
|
||||||
|
FinalEmailToCreator = settings.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = settings.FinalEmailToReceivers,
|
||||||
|
SendReminderEmails = settings.SendReminderEmails,
|
||||||
|
FirstReminderDays = settings.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = settings.ReminderIntervalDays,
|
||||||
|
ExpiresWhenDays = settings.ExpiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays,
|
||||||
|
ExpiresWhen = settings.ExpiresWhen,
|
||||||
|
ExpiresWarningWhen = settings.ExpiresWarningWhen,
|
||||||
|
ContractType = settings.ContractType
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
if (request.UseSQLExecutor)
|
||||||
|
{
|
||||||
|
await Repository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Title = title;
|
||||||
|
e.Message = message;
|
||||||
|
e.TfaEnabled = request.TFAEnabled;
|
||||||
|
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
e.Language = settings.Language;
|
||||||
|
e.UseAccessCode = settings.UseAccessCode;
|
||||||
|
e.CertificationType = settings.CertificationType;
|
||||||
|
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
e.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
e.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
e.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
e.ContractType = settings.ContractType;
|
||||||
|
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
e.ChangedWhen = now;
|
||||||
|
}, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
envelope.Title = title;
|
||||||
|
envelope.Message = message;
|
||||||
|
envelope.TfaEnabled = request.TFAEnabled;
|
||||||
|
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
envelope.Language = settings.Language;
|
||||||
|
envelope.UseAccessCode = settings.UseAccessCode;
|
||||||
|
envelope.CertificationType = settings.CertificationType;
|
||||||
|
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
envelope.ContractType = settings.ContractType;
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
envelope.ChangedWhen = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = await ResolveSenderReferenceAsync(envelope, cancel),
|
||||||
|
Status = EnvelopeStatus.EnvelopeCreated
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpsertDocumentAsync(envelope.Id, request.Document, now, cancel);
|
||||||
|
await UpsertReceiversAndFieldsAsync(envelope.Id, request.Receivers, now, cancel);
|
||||||
|
|
||||||
|
if (request.Send)
|
||||||
|
await SendEnvelopeAsync(envelope, cancel);
|
||||||
|
|
||||||
|
envelope = await Repository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.Include(e => e.EnvelopeReceivers)
|
||||||
|
.ThenInclude(er => er.Receiver)
|
||||||
|
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
return _mapper.Map<EnvelopeDto>(envelope);
|
return _mapper.Map<EnvelopeDto>(envelope);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
EnvelopeType? selectedType = null;
|
||||||
|
if (request.EnvelopeTypeId is int envelopeTypeId && envelopeTypeId > 0)
|
||||||
|
{
|
||||||
|
selectedType = await EnvelopeTypeRepository.Query
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == envelopeTypeId, cancel)
|
||||||
|
?? throw new BadRequestException($"Envelope type {envelopeTypeId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiresWhenDays = Math.Max(0,
|
||||||
|
request.ExpiresWhenDays
|
||||||
|
?? selectedType?.ExpiresWhenDays
|
||||||
|
?? existing?.ExpiresWhenDays
|
||||||
|
?? 0);
|
||||||
|
|
||||||
|
var expiresWarningWhenDays = Math.Max(0,
|
||||||
|
request.ExpiresWarningWhenDays
|
||||||
|
?? selectedType?.ExpiresWarningWhenDays
|
||||||
|
?? existing?.ExpiresWarningWhenDays
|
||||||
|
?? 0);
|
||||||
|
|
||||||
|
var useAccessCode = request.UseAccessCode
|
||||||
|
?? selectedType?.UseAccessCode
|
||||||
|
?? existing?.UseAccessCode
|
||||||
|
?? false;
|
||||||
|
|
||||||
|
if (request.TFAEnabled)
|
||||||
|
useAccessCode = true;
|
||||||
|
|
||||||
|
return new EnvelopeSettings(
|
||||||
|
EnvelopeTypeId: request.EnvelopeTypeId ?? existing?.EnvelopeTypeId,
|
||||||
|
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
||||||
|
UseAccessCode: useAccessCode,
|
||||||
|
CertificationType: request.CertificationType
|
||||||
|
?? (int?)selectedType?.CertificationType
|
||||||
|
?? existing?.CertificationType
|
||||||
|
?? (int)CertificationType.AdvancedElectronicSignature,
|
||||||
|
FinalEmailToCreator: request.FinalEmailToCreator
|
||||||
|
?? (int?)selectedType?.FinalEmailToCreator
|
||||||
|
?? existing?.FinalEmailToCreator
|
||||||
|
?? (int)FinalEmailType.No,
|
||||||
|
FinalEmailToReceivers: request.FinalEmailToReceivers
|
||||||
|
?? (int?)selectedType?.FinalEmailToReceivers
|
||||||
|
?? existing?.FinalEmailToReceivers
|
||||||
|
?? (int)FinalEmailType.No,
|
||||||
|
SendReminderEmails: request.SendReminderEmails
|
||||||
|
?? selectedType?.SendReminderEmails
|
||||||
|
?? existing?.SendReminderEmails
|
||||||
|
?? false,
|
||||||
|
FirstReminderDays: Math.Max(0,
|
||||||
|
request.FirstReminderDays
|
||||||
|
?? selectedType?.FirstReminderDays
|
||||||
|
?? existing?.FirstReminderDays
|
||||||
|
?? 0),
|
||||||
|
ReminderIntervalDays: Math.Max(0,
|
||||||
|
request.ReminderIntervalDays
|
||||||
|
?? selectedType?.ReminderIntervalDays
|
||||||
|
?? existing?.ReminderIntervalDays
|
||||||
|
?? 0),
|
||||||
|
ExpiresWhenDays: expiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays: expiresWarningWhenDays,
|
||||||
|
ExpiresWhen: expiresWhenDays > 0 ? DateTime.Now.AddDays(expiresWhenDays) : null,
|
||||||
|
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
||||||
|
ContractType: existing?.ContractType
|
||||||
|
?? selectedType?.ContractType
|
||||||
|
?? (int)ContractType.Contract);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(preferred))
|
||||||
|
return preferred.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fallback1))
|
||||||
|
return fallback1.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fallback2))
|
||||||
|
return fallback2.Trim();
|
||||||
|
|
||||||
|
return "de-DE";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record EnvelopeSettings(
|
||||||
|
int? EnvelopeTypeId,
|
||||||
|
string Language,
|
||||||
|
bool UseAccessCode,
|
||||||
|
int CertificationType,
|
||||||
|
int FinalEmailToCreator,
|
||||||
|
int FinalEmailToReceivers,
|
||||||
|
bool SendReminderEmails,
|
||||||
|
int FirstReminderDays,
|
||||||
|
int ReminderIntervalDays,
|
||||||
|
int ExpiresWhenDays,
|
||||||
|
int ExpiresWarningWhenDays,
|
||||||
|
DateTime? ExpiresWhen,
|
||||||
|
DateTime? ExpiresWarningWhen,
|
||||||
|
int ContractType);
|
||||||
|
|
||||||
|
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
|
||||||
|
return envelope.User.Email;
|
||||||
|
|
||||||
|
var senderEmail = await Repository.Query
|
||||||
|
.Where(e => e.Id == envelope.Id)
|
||||||
|
.Select(e => e.User.Email)
|
||||||
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(senderEmail))
|
||||||
|
throw new BadRequestException("Envelope sender email is missing.");
|
||||||
|
|
||||||
|
return senderEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpsertDocumentAsync(int envelopeId, CreateEnvelopeDocumentDto? document, DateTime now, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (document is null || string.IsNullOrWhiteSpace(document.DataAsBase64))
|
||||||
|
return;
|
||||||
|
|
||||||
|
byte[] bytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bytes = Convert.FromBase64String(document.DataAsBase64);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Document payload is not valid base64.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDocumentId = await DocumentRepository.Query
|
||||||
|
.Where(d => d.EnvelopeId == envelopeId)
|
||||||
|
.Select(d => (int?)d.Id)
|
||||||
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
|
||||||
|
if (existingDocumentId is null)
|
||||||
|
{
|
||||||
|
await DocumentRepository.CreateAsync(new Document
|
||||||
|
{
|
||||||
|
EnvelopeId = envelopeId,
|
||||||
|
AddedWhen = now,
|
||||||
|
ByteData = bytes,
|
||||||
|
Filename = string.Empty,
|
||||||
|
Filepath = string.Empty,
|
||||||
|
FileNameOriginal = string.Empty
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DocumentRepository.UpdateAsync(d =>
|
||||||
|
{
|
||||||
|
d.ByteData = bytes;
|
||||||
|
}, d => d.Id == existingDocumentId.Value, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpsertReceiversAndFieldsAsync(int envelopeId, IReadOnlyCollection<CreateEnvelopeReceiverDto> receivers, DateTime now, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var filteredReceivers = receivers
|
||||||
|
.Where(r => !string.IsNullOrWhiteSpace(r.EmailAddress))
|
||||||
|
.Select(r => r with
|
||||||
|
{
|
||||||
|
EmailAddress = r.EmailAddress.Trim().ToLowerInvariant(),
|
||||||
|
Name = r.Name?.Trim(),
|
||||||
|
PhoneNumber = string.IsNullOrWhiteSpace(r.PhoneNumber) ? null : r.PhoneNumber.Trim(),
|
||||||
|
Fields = r.Fields ?? new List<CreateEnvelopeFieldDto>()
|
||||||
|
})
|
||||||
|
.GroupBy(r => r.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(g => g.First())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (filteredReceivers.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var document = await DocumentRepository.Query
|
||||||
|
.FirstOrDefaultAsync(d => d.EnvelopeId == envelopeId, cancel);
|
||||||
|
|
||||||
|
if (document is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var currentLinks = await EnvelopeReceiverRepository.Query
|
||||||
|
.Include(er => er.Receiver)
|
||||||
|
.Where(er => er.EnvelopeId == envelopeId)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
var linksByEmail = currentLinks
|
||||||
|
.Where(er => !string.IsNullOrWhiteSpace(er.Receiver?.EmailAddress))
|
||||||
|
.GroupBy(er => er.Receiver!.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var sequence = currentLinks.Count > 0 ? currentLinks.Max(er => er.Sequence) : 0;
|
||||||
|
|
||||||
|
foreach (var receiverDraft in filteredReceivers)
|
||||||
|
{
|
||||||
|
var linkResult = await GetOrCreateEnvelopeReceiverAsync(envelopeId, receiverDraft, linksByEmail, now, sequence, cancel);
|
||||||
|
var link = linkResult.Link;
|
||||||
|
sequence = linkResult.NextSequence;
|
||||||
|
|
||||||
|
await DocReceiverElementRepository.DeleteAsync(query => query
|
||||||
|
.Where(e => e.DocumentId == document.Id)
|
||||||
|
.Where(e => e.ReceiverId == link.ReceiverId), cancel);
|
||||||
|
|
||||||
|
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
|
||||||
|
{
|
||||||
|
await DocReceiverElementRepository.CreateAsync(new DocReceiverElement
|
||||||
|
{
|
||||||
|
DocumentId = document.Id,
|
||||||
|
ReceiverId = link.ReceiverId,
|
||||||
|
ElementType = 1,
|
||||||
|
X = field.X,
|
||||||
|
Y = field.Y,
|
||||||
|
Width = 1.77,
|
||||||
|
Height = 1.96,
|
||||||
|
Page = field.Page,
|
||||||
|
Required = true,
|
||||||
|
ReadOnly = false,
|
||||||
|
AnnotationIndex = 0,
|
||||||
|
AddedWhen = now,
|
||||||
|
FullName = link.Name,
|
||||||
|
Position = string.Empty,
|
||||||
|
Place = string.Empty,
|
||||||
|
Tooltip = string.Empty
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(EnvelopeReceiver Link, int NextSequence)> GetOrCreateEnvelopeReceiverAsync(
|
||||||
|
int envelopeId,
|
||||||
|
CreateEnvelopeReceiverDto receiverDraft,
|
||||||
|
IDictionary<string, EnvelopeReceiver> linksByEmail,
|
||||||
|
DateTime now,
|
||||||
|
int sequence,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (linksByEmail.TryGetValue(receiverDraft.EmailAddress, out var existingLink))
|
||||||
|
{
|
||||||
|
var nextName = string.IsNullOrWhiteSpace(receiverDraft.Name) ? existingLink.Name : receiverDraft.Name!;
|
||||||
|
var nextPhoneNumber = receiverDraft.PhoneNumber ?? existingLink.PhoneNumber;
|
||||||
|
|
||||||
|
await EnvelopeReceiverRepository.UpdateAsync(er =>
|
||||||
|
{
|
||||||
|
er.Name = nextName;
|
||||||
|
er.PhoneNumber = nextPhoneNumber;
|
||||||
|
er.ChangedWhen = now;
|
||||||
|
}, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel);
|
||||||
|
|
||||||
|
existingLink.Name = nextName;
|
||||||
|
existingLink.PhoneNumber = nextPhoneNumber;
|
||||||
|
return (existingLink, sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
var receiver = await ReceiverRepository.Query
|
||||||
|
.FirstOrDefaultAsync(r => r.EmailAddress != null && r.EmailAddress.ToLower() == receiverDraft.EmailAddress, cancel);
|
||||||
|
|
||||||
|
if (receiver is null)
|
||||||
|
{
|
||||||
|
receiver = await ReceiverRepository.CreateAsync(new Receiver
|
||||||
|
{
|
||||||
|
EmailAddress = receiverDraft.EmailAddress,
|
||||||
|
Signature = receiverDraft.EmailAddress.ToUpperInvariant().GetChecksum(),
|
||||||
|
AddedWhen = now,
|
||||||
|
TotpSecretkey = string.Empty,
|
||||||
|
TfaRegDeadline = null
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence++;
|
||||||
|
var link = await EnvelopeReceiverRepository.CreateAsync(new EnvelopeReceiver
|
||||||
|
{
|
||||||
|
EnvelopeId = envelopeId,
|
||||||
|
ReceiverId = receiver.Id,
|
||||||
|
Sequence = sequence,
|
||||||
|
Name = receiverDraft.Name ?? receiverDraft.EmailAddress,
|
||||||
|
JobTitle = string.Empty,
|
||||||
|
CompanyName = string.Empty,
|
||||||
|
PrivateMessage = string.Empty,
|
||||||
|
AccessCode = string.Empty,
|
||||||
|
AddedWhen = now,
|
||||||
|
ChangedWhen = now,
|
||||||
|
PhoneNumber = receiverDraft.PhoneNumber ?? string.Empty
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
link.Receiver = receiver;
|
||||||
|
linksByEmail[receiverDraft.EmailAddress] = link;
|
||||||
|
return (link, sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var hasDocument = await DocumentRepository.Query
|
||||||
|
.AnyAsync(d => d.EnvelopeId == envelope.Id && d.ByteData != null && d.ByteData.Length > 0, cancel);
|
||||||
|
|
||||||
|
if (!hasDocument)
|
||||||
|
throw new BadRequestException("Cannot send envelope without a document.");
|
||||||
|
|
||||||
|
var links = await EnvelopeReceiverRepository.Query
|
||||||
|
.Include(er => er.Receiver)
|
||||||
|
.Where(er => er.EnvelopeId == envelope.Id)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
if (links.Count == 0)
|
||||||
|
throw new BadRequestException("Cannot send envelope without receivers.");
|
||||||
|
|
||||||
|
var receiverIds = links.Select(l => l.ReceiverId).Distinct().ToList();
|
||||||
|
var documentIds = await DocumentRepository.Query
|
||||||
|
.Where(d => d.EnvelopeId == envelope.Id)
|
||||||
|
.Select(d => d.Id)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
var receiverFieldCounts = await DocReceiverElementRepository.Query
|
||||||
|
.Where(e => documentIds.Contains(e.DocumentId))
|
||||||
|
.Where(e => receiverIds.Contains(e.ReceiverId))
|
||||||
|
.GroupBy(e => e.ReceiverId)
|
||||||
|
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
||||||
|
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
||||||
|
|
||||||
|
var receiversWithoutFields = links
|
||||||
|
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
||||||
|
.Select(link => link.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (receiversWithoutFields.Count > 0)
|
||||||
|
throw new BadRequestException("Cannot send envelope because some receivers have no signature fields.");
|
||||||
|
|
||||||
|
var changedWhen = DateTime.Now;
|
||||||
|
await Repository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Status = EnvelopeStatus.EnvelopeQueued;
|
||||||
|
e.ChangedWhen = changedWhen;
|
||||||
|
}, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
var userReference = await ResolveSenderReferenceAsync(envelope, cancel);
|
||||||
|
|
||||||
|
await Mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = userReference,
|
||||||
|
Status = EnvelopeStatus.EnvelopeQueued
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
await QueueInvitationEmailsAsync(envelope, links, userReference, cancel);
|
||||||
|
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task QueueInvitationEmailsAsync(
|
||||||
|
Envelope envelope,
|
||||||
|
IReadOnlyCollection<EnvelopeReceiver> links,
|
||||||
|
string senderReference,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var template = await EmailTemplateRepository.Query
|
||||||
|
.SingleOrDefaultAsync(x => x.Name == EmailTemplateType.DocumentReceived.ToString(), cancel)
|
||||||
|
?? throw new InvalidOperationException($"Email template not found: {EmailTemplateType.DocumentReceived}");
|
||||||
|
|
||||||
|
var signatureHost = await ConfigService.ReadDefaultSignatureHost();
|
||||||
|
var senderName = senderReference;
|
||||||
|
|
||||||
|
foreach (var link in links)
|
||||||
|
{
|
||||||
|
var receiver = link.Receiver;
|
||||||
|
if (receiver is null || string.IsNullOrWhiteSpace(receiver.EmailAddress) || string.IsNullOrWhiteSpace(receiver.Signature))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var envelopeKey = (envelope.Uuid, receiver.Signature).ToEnvelopeKey();
|
||||||
|
var documentLink = $"{signatureHost.TrimEnd('/')}/envelope/login/{Uri.EscapeDataString(envelopeKey)}";
|
||||||
|
|
||||||
|
var placeholders = new Dictionary<string, string>(MailParams.Placeholders)
|
||||||
|
{
|
||||||
|
["[NAME_RECEIVER]"] = link.Name ?? string.Empty,
|
||||||
|
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||||
|
["[DOCUMENT_TITLE]"] = envelope.Title ?? string.Empty,
|
||||||
|
["[MESSAGE]"] = envelope.Message ?? string.Empty,
|
||||||
|
["[LINK_TO_DOCUMENT]"] = documentLink,
|
||||||
|
["[LINK_TO_DOCUMENT_TEXT]"] = documentLink[..Math.Min(40, documentLink.Length)] + ".."
|
||||||
|
};
|
||||||
|
|
||||||
|
var subject = ReplacePlaceHolders(template.Subject, placeholders);
|
||||||
|
var body = ReplacePlaceHolders(template.Body, placeholders);
|
||||||
|
|
||||||
|
var emailOut = new EmailOut
|
||||||
|
{
|
||||||
|
EmailAddress = receiver.EmailAddress,
|
||||||
|
EmailSubj = subject,
|
||||||
|
EmailBody = body,
|
||||||
|
AddedWhen = DateTime.Now,
|
||||||
|
AddedWho = DispatcherParams.AddedWho,
|
||||||
|
SendingProfile = DispatcherParams.SendingProfile,
|
||||||
|
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
||||||
|
EmailAttmt1 = DispatcherParams.EmailAttmt1,
|
||||||
|
WfId = (int)EnvelopeStatus.MessageInvitationSent,
|
||||||
|
ReferenceString = receiver.EmailAddress,
|
||||||
|
ReferenceId = link.ReceiverId
|
||||||
|
};
|
||||||
|
|
||||||
|
await EmailOutRepository.CreateAsync(emailOut, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReplacePlaceHolders(string text, Dictionary<string, string> placeHolders)
|
||||||
|
{
|
||||||
|
foreach (var placeHolder in placeHolders)
|
||||||
|
text = text.Replace(placeHolder.Key, placeHolder.Value);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
|
||||||
|
|
||||||
|
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MinLength(1)]
|
||||||
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
internal int UserId { get; private set; }
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommand WithAuth(int userId)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Envelope> _envelopeRepository;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommandHandler(
|
||||||
|
IRepository<Envelope> envelopeRepository,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_envelopeRepository = envelopeRepository;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var reason = request.Reason?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
throw new BadRequestException("Please provide a reason.");
|
||||||
|
|
||||||
|
var envelope = await _envelopeRepository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.Include(e => e.EnvelopeReceivers)
|
||||||
|
.ThenInclude(er => er.Receiver)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
||||||
|
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
||||||
|
|
||||||
|
if (envelope.UserId != request.UserId)
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
||||||
|
|
||||||
|
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
||||||
|
? EnvelopeStatus.EnvelopeWithdrawn
|
||||||
|
: EnvelopeStatus.EnvelopeDeleted;
|
||||||
|
|
||||||
|
var changedWhen = DateTime.Now;
|
||||||
|
envelope.Status = targetStatus;
|
||||||
|
envelope.Comment = reason;
|
||||||
|
envelope.ChangedWhen = changedWhen;
|
||||||
|
|
||||||
|
await _envelopeRepository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Status = targetStatus;
|
||||||
|
e.Comment = reason;
|
||||||
|
e.ChangedWhen = changedWhen;
|
||||||
|
}, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
var userReference = envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(userReference))
|
||||||
|
throw new BadRequestException("Envelope sender email is missing.");
|
||||||
|
|
||||||
|
await _mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = userReference,
|
||||||
|
Status = targetStatus,
|
||||||
|
Comment = reason
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||||
|
{
|
||||||
|
if (envelopeReceiver.Receiver is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await _mediator.Publish(new EnvelopeDeletedNotification
|
||||||
|
{
|
||||||
|
Envelope = envelope,
|
||||||
|
EnvelopeReceiver = envelopeReceiver,
|
||||||
|
Reason = reason,
|
||||||
|
SenderReference = userReference
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,9 +149,11 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
query = query.Where(e => Status.Completed.Contains(e.Status));
|
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
|
.Include(e => e.Type)
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
|
.Include(e => e.Histories)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -651,4 +651,125 @@
|
|||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Status</value>
|
<value>Status</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="ChangedAt" xml:space="preserve"><value>Geändert am</value></data>
|
||||||
|
<data name="Close" xml:space="preserve"><value>Schließen</value></data>
|
||||||
|
<data name="CloseLanguageMenu" xml:space="preserve"><value>Sprachmenü schließen</value></data>
|
||||||
|
<data name="ConfirmSignature" xml:space="preserve"><value>Unterschrift bestätigen</value></data>
|
||||||
|
<data name="Created" xml:space="preserve"><value>Erstellt</value></data>
|
||||||
|
<data name="Delete" xml:space="preserve"><value>Löschen</value></data>
|
||||||
|
<data name="Document" xml:space="preserve"><value>Dokument</value></data>
|
||||||
|
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Dokument konnte nicht geladen werden</value></data>
|
||||||
|
<data name="DocumentLoading" xml:space="preserve"><value>Dokument wird geladen...</value></data>
|
||||||
|
<data name="EmailAddress" xml:space="preserve"><value>E-Mail-Adresse</value></data>
|
||||||
|
<data name="Envelope" xml:space="preserve"><value>Umschlag</value></data>
|
||||||
|
<data name="Error" xml:space="preserve"><value>Fehler</value></data>
|
||||||
|
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Fehler beim Laden des Dokuments: {0}</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Entwicklungsmodus</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Dies kann dazu führen, dass vertrauliche Informationen aus Ausnahmen Endbenutzern angezeigt werden. Für lokales Debugging setzen Sie ASPNETCORE_ENVIRONMENT auf Development und starten die App neu.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Im Entwicklungsmodus werden detailliertere Fehlerinformationen angezeigt.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Der Entwicklungsmodus sollte in bereitgestellten Anwendungen nicht aktiviert sein.</value></data>
|
||||||
|
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten.</value></data>
|
||||||
|
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Anfrage-ID</value></data>
|
||||||
|
<data name="ErrorWithReason" xml:space="preserve"><value>Fehler: {0}</value></data>
|
||||||
|
<data name="From" xml:space="preserve"><value>Von</value></data>
|
||||||
|
<data name="FullName" xml:space="preserve"><value>Vor- und Nachname</value></data>
|
||||||
|
<data name="History" xml:space="preserve"><value>Historie</value></data>
|
||||||
|
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digitale Unterschrift</value></data>
|
||||||
|
<data name="Index_PdfExport" xml:space="preserve"><value>PDF-Export</value></data>
|
||||||
|
<data name="Index_SecureAccess" xml:space="preserve"><value>Sicherer Zugang</value></data>
|
||||||
|
<data name="Index_Subtitle" xml:space="preserve"><value>Willkommen im eSign-Portal</value></data>
|
||||||
|
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Zur Umschlag-Übersicht</value></data>
|
||||||
|
<data name="Languages" xml:space="preserve"><value>Sprachen</value></data>
|
||||||
|
<data name="Loading" xml:space="preserve"><value>Lädt...</value></data>
|
||||||
|
<data name="Login" xml:space="preserve"><value>Anmelden</value></data>
|
||||||
|
<data name="LoginInProgress" xml:space="preserve"><value>Anmelden ...</value></data>
|
||||||
|
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Nicht authentifiziert. Bitte melden Sie sich erneut an.</value></data>
|
||||||
|
<data name="Only" xml:space="preserve"><value>Nur</value></data>
|
||||||
|
<data name="OpenDocument" xml:space="preserve"><value>Dokument öffnen</value></data>
|
||||||
|
<data name="Optional" xml:space="preserve"><value>optional</value></data>
|
||||||
|
<data name="Pending" xml:space="preserve"><value>Ausstehend</value></data>
|
||||||
|
<data name="Preview" xml:space="preserve"><value>Vorschau</value></data>
|
||||||
|
<data name="PrivacyPolicy" xml:space="preserve"><value>Datenschutz</value></data>
|
||||||
|
<data name="Receiver" xml:space="preserve"><value>Empfänger</value></data>
|
||||||
|
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Zugangscode eingeben</value></data>
|
||||||
|
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.</value></data>
|
||||||
|
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Ungültiger Zugangscode.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Dokument nicht gefunden.</value></data>
|
||||||
|
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang mit Zugangscode</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Möchten Sie das Dokument verbindlich unterschreiben?</value></data>
|
||||||
|
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope-Schlüssel fehlt.</value></data>
|
||||||
|
<data name="Refresh" xml:space="preserve"><value>Aktualisieren</value></data>
|
||||||
|
<data name="RequiredField" xml:space="preserve"><value>Pflichtfeld</value></data>
|
||||||
|
<data name="Selected" xml:space="preserve"><value>ausgewählte</value></data>
|
||||||
|
<data name="Send" xml:space="preserve"><value>Senden</value></data>
|
||||||
|
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Aktive Umschläge</value></data>
|
||||||
|
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Abgeschlossene Umschläge</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Neuer Umschlag</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Neuen Umschlag erstellen</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Umschlag löschen</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Begründung eingeben...</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Bitte geben Sie einen Löschgrund an.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag löschen</value></data>
|
||||||
|
<data name="Sender_Edit" xml:space="preserve"><value>Bearbeiten</value></data>
|
||||||
|
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag bearbeiten</value></data>
|
||||||
|
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Aktuelle Ansicht exportieren</value></data>
|
||||||
|
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Exportformat auswählen</value></data>
|
||||||
|
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export der aktuellen Tabellenansicht</value></data>
|
||||||
|
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Fehler beim Laden der Umschläge</value></data>
|
||||||
|
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Umschläge werden geladen...</value></data>
|
||||||
|
<data name="Sender_NoHistory" xml:space="preserve"><value>Keine Historie vorhanden</value></data>
|
||||||
|
<data name="Sender_NoReceivers" xml:space="preserve"><value>Keine Empfänger</value></data>
|
||||||
|
<data name="Sender_Preview_Title" xml:space="preserve"><value>Ausgewählten Umschlag als PDF anzeigen</value></data>
|
||||||
|
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf.</value></data>
|
||||||
|
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Signatur hinzufügen</value></data>
|
||||||
|
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Klicken Sie auf die Stelle im Dokument für</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>E-Mail-Vorschläge werden geladen...</value></data>
|
||||||
|
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Fehler beim Erstellen des Umschlags</value></data>
|
||||||
|
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Fehler beim Laden des Umschlags #{0}: {1}</value></data>
|
||||||
|
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Fehler beim Laden der Datei: {0}</value></data>
|
||||||
|
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Bitte unterzeichnen Sie das beigefügte Dokument.</value></data>
|
||||||
|
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Kein Dokument geladen</value></data>
|
||||||
|
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Keine Seiten zur Anzeige verfügbar.</value></data>
|
||||||
|
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Noch keine Empfänger hinzugefügt.</value></data>
|
||||||
|
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Seite</value></data>
|
||||||
|
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Signaturfeld platzieren</value></data>
|
||||||
|
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Alle Signaturfelder entfernen</value></data>
|
||||||
|
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Bitte wählen Sie eine PDF-Datei aus.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Der bestehende Entwurf wurde versendet.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Umschlag gesendet</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Umschlag wurde erfolgreich gesendet.</value></data>
|
||||||
|
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signaturfeld</value></data>
|
||||||
|
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>z.B. Arbeitsvertrag 2025</value></data>
|
||||||
|
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Zur Übersicht</value></data>
|
||||||
|
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Diese E-Mail-Adresse wurde bereits hinzugefügt.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Bitte geben Sie eine gültige E-Mail-Adresse ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Bitte geben Sie eine E-Mail-Adresse ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Bitte geben Sie einen Vor- und Nachnamen ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Bitte fügen Sie mindestens einen Empfänger hinzu.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Bitte platzieren Sie mindestens ein Signaturfeld.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Bitte geben Sie einen Titel ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Bitte laden Sie zuerst ein PDF-Dokument hoch.</value></data>
|
||||||
|
<data name="SenderLogin_Description" xml:space="preserve"><value>Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.</value></data>
|
||||||
|
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Administrator.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Ungültige Anmeldedaten.</value></data>
|
||||||
|
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Passwort eingeben</value></data>
|
||||||
|
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang zum Sender-Dashboard</value></data>
|
||||||
|
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender Anmeldung</value></data>
|
||||||
|
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Benutzername eingeben</value></data>
|
||||||
|
<data name="ServerError" xml:space="preserve"><value>Serverfehler.</value></data>
|
||||||
|
<data name="Signed" xml:space="preserve"><value>Unterschrieben</value></data>
|
||||||
|
<data name="SignedDocument" xml:space="preserve"><value>Signiertes Dokument</value></data>
|
||||||
|
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||||
|
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</value></data>
|
||||||
|
<data name="UploadPdf" xml:space="preserve"><value>PDF hochladen</value></data>
|
||||||
|
<data name="UserName" xml:space="preserve"><value>Benutzername</value></data>
|
||||||
|
<data name="VerifyInProgress" xml:space="preserve"><value>Überprüfen ...</value></data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -651,4 +651,125 @@
|
|||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Status</value>
|
<value>Status</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="ChangedAt" xml:space="preserve"><value>Changed at</value></data>
|
||||||
|
<data name="Close" xml:space="preserve"><value>Close</value></data>
|
||||||
|
<data name="CloseLanguageMenu" xml:space="preserve"><value>Close language menu</value></data>
|
||||||
|
<data name="ConfirmSignature" xml:space="preserve"><value>Confirm signature</value></data>
|
||||||
|
<data name="Created" xml:space="preserve"><value>Created</value></data>
|
||||||
|
<data name="Delete" xml:space="preserve"><value>Delete</value></data>
|
||||||
|
<data name="Document" xml:space="preserve"><value>Document</value></data>
|
||||||
|
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Document could not be loaded</value></data>
|
||||||
|
<data name="DocumentLoading" xml:space="preserve"><value>Document is loading...</value></data>
|
||||||
|
<data name="EmailAddress" xml:space="preserve"><value>Email address</value></data>
|
||||||
|
<data name="Envelope" xml:space="preserve"><value>Envelope</value></data>
|
||||||
|
<data name="Error" xml:space="preserve"><value>Error</value></data>
|
||||||
|
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Error loading document: {0}</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Development Mode</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>This can result in sensitive exception information being shown to end users. For local debugging, set ASPNETCORE_ENVIRONMENT to Development and restart the app.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Switching to Development will display more detailed error information.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>The Development environment should not be enabled in deployed applications.</value></data>
|
||||||
|
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>An error occurred while processing your request.</value></data>
|
||||||
|
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Request ID</value></data>
|
||||||
|
<data name="ErrorWithReason" xml:space="preserve"><value>Error: {0}</value></data>
|
||||||
|
<data name="From" xml:space="preserve"><value>From</value></data>
|
||||||
|
<data name="FullName" xml:space="preserve"><value>First and last name</value></data>
|
||||||
|
<data name="History" xml:space="preserve"><value>History</value></data>
|
||||||
|
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digital signature</value></data>
|
||||||
|
<data name="Index_PdfExport" xml:space="preserve"><value>PDF export</value></data>
|
||||||
|
<data name="Index_SecureAccess" xml:space="preserve"><value>Secure access</value></data>
|
||||||
|
<data name="Index_Subtitle" xml:space="preserve"><value>Welcome to the eSign portal</value></data>
|
||||||
|
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Go to envelope overview</value></data>
|
||||||
|
<data name="Languages" xml:space="preserve"><value>Languages</value></data>
|
||||||
|
<data name="Loading" xml:space="preserve"><value>Loading...</value></data>
|
||||||
|
<data name="Login" xml:space="preserve"><value>Sign in</value></data>
|
||||||
|
<data name="LoginInProgress" xml:space="preserve"><value>Signing in ...</value></data>
|
||||||
|
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Not authenticated. Please sign in again.</value></data>
|
||||||
|
<data name="Only" xml:space="preserve"><value>Only</value></data>
|
||||||
|
<data name="OpenDocument" xml:space="preserve"><value>Open document</value></data>
|
||||||
|
<data name="Optional" xml:space="preserve"><value>optional</value></data>
|
||||||
|
<data name="Pending" xml:space="preserve"><value>Pending</value></data>
|
||||||
|
<data name="Preview" xml:space="preserve"><value>Preview</value></data>
|
||||||
|
<data name="PrivacyPolicy" xml:space="preserve"><value>Privacy policy</value></data>
|
||||||
|
<data name="Receiver" xml:space="preserve"><value>Receiver</value></data>
|
||||||
|
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Enter access code</value></data>
|
||||||
|
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Please enter the access code you received by email to securely open the document.</value></data>
|
||||||
|
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the document sender.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>The entered code is incorrect. Please try again.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Invalid access code.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>The provided access code could not be matched to a document. Please verify the link in your email.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document not found.</value></data>
|
||||||
|
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Secure access with access code</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>This action cannot be undone. By confirming, you declare that you have electronically signed the document shown above. The signed document will then be delivered to all involved parties.</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Do you want to sign this document as binding?</value></data>
|
||||||
|
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope key is missing.</value></data>
|
||||||
|
<data name="Refresh" xml:space="preserve"><value>Refresh</value></data>
|
||||||
|
<data name="RequiredField" xml:space="preserve"><value>Required field</value></data>
|
||||||
|
<data name="Selected" xml:space="preserve"><value>selected</value></data>
|
||||||
|
<data name="Send" xml:space="preserve"><value>Send</value></data>
|
||||||
|
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Active envelopes</value></data>
|
||||||
|
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Completed envelopes</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>New envelope</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Create new envelope</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Delete envelope</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Enter reason...</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Please provide a reason why the envelope should be deleted/withdrawn.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Please provide a deletion reason.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Delete selected envelope</value></data>
|
||||||
|
<data name="Sender_Edit" xml:space="preserve"><value>Edit</value></data>
|
||||||
|
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Edit selected envelope</value></data>
|
||||||
|
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Export current view</value></data>
|
||||||
|
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Select export format</value></data>
|
||||||
|
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export current table view</value></data>
|
||||||
|
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Error loading envelopes</value></data>
|
||||||
|
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Loading envelopes...</value></data>
|
||||||
|
<data name="Sender_NoHistory" xml:space="preserve"><value>No history available</value></data>
|
||||||
|
<data name="Sender_NoReceivers" xml:space="preserve"><value>No receivers</value></data>
|
||||||
|
<data name="Sender_Preview_Title" xml:space="preserve"><value>Show selected envelope as PDF</value></data>
|
||||||
|
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>The document for envelope #{0} was not found or you do not have access.</value></data>
|
||||||
|
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Add signature</value></data>
|
||||||
|
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Click on the location in the document for</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Previously used email addresses are suggested while typing.</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Loading email suggestions...</value></data>
|
||||||
|
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Error creating envelope</value></data>
|
||||||
|
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Error loading envelope #{0}: {1}</value></data>
|
||||||
|
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Error loading file: {0}</value></data>
|
||||||
|
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Please sign the attached document.</value></data>
|
||||||
|
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>No document loaded</value></data>
|
||||||
|
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>No pages to display.</value></data>
|
||||||
|
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>No receivers added yet.</value></data>
|
||||||
|
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||||
|
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Place signature field</value></data>
|
||||||
|
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Remove all signature fields</value></data>
|
||||||
|
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Please select a PDF file.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>The existing draft has been sent.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Envelope sent</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Envelope was sent successfully.</value></data>
|
||||||
|
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signature field</value></data>
|
||||||
|
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>e.g. Employment contract 2025</value></data>
|
||||||
|
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>To overview</value></data>
|
||||||
|
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Upload a PDF file to place signature fields.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>This email address has already been added.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Please enter a valid email address.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Please enter an email address.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Please enter first and last name.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Please add at least one receiver.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Please place at least one signature field.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Please enter a title.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Please upload a PDF document first.</value></data>
|
||||||
|
<data name="SenderLogin_Description" xml:space="preserve"><value>Please sign in with your credentials to access the sender dashboard.</value></data>
|
||||||
|
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the administrator.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Username or password is incorrect. Please try again.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Invalid credentials.</value></data>
|
||||||
|
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Enter password</value></data>
|
||||||
|
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Secure access to sender dashboard</value></data>
|
||||||
|
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender login</value></data>
|
||||||
|
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Enter username</value></data>
|
||||||
|
<data name="ServerError" xml:space="preserve"><value>Server error.</value></data>
|
||||||
|
<data name="Signed" xml:space="preserve"><value>Signed</value></data>
|
||||||
|
<data name="SignedDocument" xml:space="preserve"><value>Signed document</value></data>
|
||||||
|
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||||
|
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>An unexpected error occurred. Please try again later.</value></data>
|
||||||
|
<data name="UploadPdf" xml:space="preserve"><value>Upload PDF</value></data>
|
||||||
|
<data name="UserName" xml:space="preserve"><value>Username</value></data>
|
||||||
|
<data name="VerifyInProgress" xml:space="preserve"><value>Verifying ...</value></data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -651,4 +651,125 @@
|
|||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Statut</value>
|
<value>Statut</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="ChangedAt" xml:space="preserve"><value>Modifié le</value></data>
|
||||||
|
<data name="Close" xml:space="preserve"><value>Fermer</value></data>
|
||||||
|
<data name="CloseLanguageMenu" xml:space="preserve"><value>Fermer le menu des langues</value></data>
|
||||||
|
<data name="ConfirmSignature" xml:space="preserve"><value>Confirmer la signature</value></data>
|
||||||
|
<data name="Created" xml:space="preserve"><value>Créé</value></data>
|
||||||
|
<data name="Delete" xml:space="preserve"><value>Supprimer</value></data>
|
||||||
|
<data name="Document" xml:space="preserve"><value>Document</value></data>
|
||||||
|
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Le document n'a pas pu être chargé</value></data>
|
||||||
|
<data name="DocumentLoading" xml:space="preserve"><value>Chargement du document...</value></data>
|
||||||
|
<data name="EmailAddress" xml:space="preserve"><value>Adresse e-mail</value></data>
|
||||||
|
<data name="Envelope" xml:space="preserve"><value>Enveloppe</value></data>
|
||||||
|
<data name="Error" xml:space="preserve"><value>Erreur</value></data>
|
||||||
|
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Erreur lors du chargement du document : {0}</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Mode développement</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Cela peut afficher des informations sensibles d'exception aux utilisateurs finaux. Pour le débogage local, définissez ASPNETCORE_ENVIRONMENT sur Development et redémarrez l'application.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Le mode développement affiche des informations d'erreur plus détaillées.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Le mode développement ne doit pas être activé dans les applications déployées.</value></data>
|
||||||
|
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Une erreur s'est produite lors du traitement de votre demande.</value></data>
|
||||||
|
<data name="ErrorPage_RequestId" xml:space="preserve"><value>ID de requête</value></data>
|
||||||
|
<data name="ErrorWithReason" xml:space="preserve"><value>Erreur : {0}</value></data>
|
||||||
|
<data name="From" xml:space="preserve"><value>De</value></data>
|
||||||
|
<data name="FullName" xml:space="preserve"><value>Nom et prénom</value></data>
|
||||||
|
<data name="History" xml:space="preserve"><value>Historique</value></data>
|
||||||
|
<data name="Index_DigitalSignature" xml:space="preserve"><value>Signature numérique</value></data>
|
||||||
|
<data name="Index_PdfExport" xml:space="preserve"><value>Export PDF</value></data>
|
||||||
|
<data name="Index_SecureAccess" xml:space="preserve"><value>Accès sécurisé</value></data>
|
||||||
|
<data name="Index_Subtitle" xml:space="preserve"><value>Bienvenue sur le portail eSign</value></data>
|
||||||
|
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Aller à l'aperçu des enveloppes</value></data>
|
||||||
|
<data name="Languages" xml:space="preserve"><value>Langues</value></data>
|
||||||
|
<data name="Loading" xml:space="preserve"><value>Chargement...</value></data>
|
||||||
|
<data name="Login" xml:space="preserve"><value>Connexion</value></data>
|
||||||
|
<data name="LoginInProgress" xml:space="preserve"><value>Connexion ...</value></data>
|
||||||
|
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Non authentifié. Veuillez vous reconnecter.</value></data>
|
||||||
|
<data name="Only" xml:space="preserve"><value>Uniquement</value></data>
|
||||||
|
<data name="OpenDocument" xml:space="preserve"><value>Ouvrir le document</value></data>
|
||||||
|
<data name="Optional" xml:space="preserve"><value>optionnel</value></data>
|
||||||
|
<data name="Pending" xml:space="preserve"><value>En attente</value></data>
|
||||||
|
<data name="Preview" xml:space="preserve"><value>Aperçu</value></data>
|
||||||
|
<data name="PrivacyPolicy" xml:space="preserve"><value>Politique de confidentialité</value></data>
|
||||||
|
<data name="Receiver" xml:space="preserve"><value>Destinataire</value></data>
|
||||||
|
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Saisir le code d'accès</value></data>
|
||||||
|
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Veuillez saisir le code d'accès reçu par e-mail pour ouvrir le document en toute sécurité.</value></data>
|
||||||
|
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'expéditeur du document.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Le code saisi est incorrect. Veuillez réessayer.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Code d'accès invalide.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Le code d'accès indiqué ne correspond à aucun document. Veuillez vérifier le lien dans votre e-mail.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document introuvable.</value></data>
|
||||||
|
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé avec code d'accès</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Cette action est irréversible. En confirmant, vous déclarez avoir signé électroniquement le document affiché ci-dessus. Le document signé sera ensuite transmis à toutes les parties concernées.</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Voulez-vous signer ce document de manière contraignante ?</value></data>
|
||||||
|
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>La clé d'enveloppe est manquante.</value></data>
|
||||||
|
<data name="Refresh" xml:space="preserve"><value>Actualiser</value></data>
|
||||||
|
<data name="RequiredField" xml:space="preserve"><value>Champ obligatoire</value></data>
|
||||||
|
<data name="Selected" xml:space="preserve"><value>sélectionnées</value></data>
|
||||||
|
<data name="Send" xml:space="preserve"><value>Envoyer</value></data>
|
||||||
|
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Enveloppes actives</value></data>
|
||||||
|
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Enveloppes terminées</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Nouvelle enveloppe</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Créer une nouvelle enveloppe</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Supprimer l'enveloppe</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Saisir la raison...</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Veuillez indiquer la raison pour laquelle l'enveloppe doit être supprimée/retirée.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Veuillez indiquer un motif de suppression.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Supprimer l'enveloppe sélectionnée</value></data>
|
||||||
|
<data name="Sender_Edit" xml:space="preserve"><value>Modifier</value></data>
|
||||||
|
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Modifier l'enveloppe sélectionnée</value></data>
|
||||||
|
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Exporter la vue actuelle</value></data>
|
||||||
|
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Sélectionner le format d'export</value></data>
|
||||||
|
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Exporter la vue actuelle du tableau</value></data>
|
||||||
|
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Erreur lors du chargement des enveloppes</value></data>
|
||||||
|
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Chargement des enveloppes...</value></data>
|
||||||
|
<data name="Sender_NoHistory" xml:space="preserve"><value>Aucun historique disponible</value></data>
|
||||||
|
<data name="Sender_NoReceivers" xml:space="preserve"><value>Aucun destinataire</value></data>
|
||||||
|
<data name="Sender_Preview_Title" xml:space="preserve"><value>Afficher l'enveloppe sélectionnée en PDF</value></data>
|
||||||
|
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Le document de l'enveloppe #{0} est introuvable ou vous n'avez pas l'autorisation d'y accéder.</value></data>
|
||||||
|
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Ajouter une signature</value></data>
|
||||||
|
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Cliquez sur l'emplacement dans le document pour</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Les adresses e-mail déjà utilisées sont proposées pendant la saisie.</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Chargement des suggestions d'e-mail...</value></data>
|
||||||
|
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Erreur lors de la création de l'enveloppe</value></data>
|
||||||
|
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Erreur lors du chargement de l'enveloppe #{0} : {1}</value></data>
|
||||||
|
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Erreur lors du chargement du fichier : {0}</value></data>
|
||||||
|
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Veuillez signer le document ci-joint.</value></data>
|
||||||
|
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Aucun document chargé</value></data>
|
||||||
|
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Aucune page à afficher.</value></data>
|
||||||
|
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Aucun destinataire ajouté pour le moment.</value></data>
|
||||||
|
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||||
|
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Placer un champ de signature</value></data>
|
||||||
|
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Supprimer tous les champs de signature</value></data>
|
||||||
|
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Veuillez sélectionner un fichier PDF.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Le brouillon existant a été envoyé.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Enveloppe envoyée</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>L'enveloppe a été envoyée avec succès.</value></data>
|
||||||
|
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Champ de signature</value></data>
|
||||||
|
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>p. ex. Contrat de travail 2025</value></data>
|
||||||
|
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Vers l'aperçu</value></data>
|
||||||
|
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Téléversez un fichier PDF pour placer des champs de signature.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Cette adresse e-mail a déjà été ajoutée.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Veuillez saisir une adresse e-mail valide.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Veuillez saisir une adresse e-mail.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Veuillez saisir le nom et le prénom.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Veuillez ajouter au moins un destinataire.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Veuillez placer au moins un champ de signature.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Veuillez saisir un titre.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Veuillez d'abord téléverser un document PDF.</value></data>
|
||||||
|
<data name="SenderLogin_Description" xml:space="preserve"><value>Veuillez vous connecter avec vos identifiants pour accéder au tableau de bord expéditeur.</value></data>
|
||||||
|
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'administrateur.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Le nom d'utilisateur ou le mot de passe est incorrect. Veuillez réessayer.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Identifiants invalides.</value></data>
|
||||||
|
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Saisir le mot de passe</value></data>
|
||||||
|
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé au tableau de bord expéditeur</value></data>
|
||||||
|
<data name="SenderLogin_Title" xml:space="preserve"><value>Connexion expéditeur</value></data>
|
||||||
|
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Saisir le nom d'utilisateur</value></data>
|
||||||
|
<data name="ServerError" xml:space="preserve"><value>Erreur serveur.</value></data>
|
||||||
|
<data name="Signed" xml:space="preserve"><value>Signé</value></data>
|
||||||
|
<data name="SignedDocument" xml:space="preserve"><value>Document signé</value></data>
|
||||||
|
<data name="System" xml:space="preserve"><value>Système</value></data>
|
||||||
|
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Une erreur inattendue s'est produite. Veuillez réessayer plus tard.</value></data>
|
||||||
|
<data name="UploadPdf" xml:space="preserve"><value>Téléverser un PDF</value></data>
|
||||||
|
<data name="UserName" xml:space="preserve"><value>Nom d'utilisateur</value></data>
|
||||||
|
<data name="VerifyInProgress" xml:space="preserve"><value>Vérification ...</value></data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -22,41 +22,41 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public string Language { get; set; }
|
public string Language { get; set; }
|
||||||
|
|
||||||
[Column("EXPIRES_DAYS")]
|
[Column("EXPIRES_DAYS")]
|
||||||
public int ExpiresDays { get; set; }
|
public int? ExpiresDays { get; set; }
|
||||||
|
|
||||||
[Column("CERTIFICATION_TYPE")]
|
[Column("CERTIFICATION_TYPE")]
|
||||||
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature;
|
public Constants.CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
[Column("USE_ACCESS_CODE")]
|
[Column("USE_ACCESS_CODE")]
|
||||||
public bool UseAccessCode { get; set; }
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_CREATOR")]
|
[Column("FINAL_EMAIL_TO_CREATOR")]
|
||||||
public Constants.FinalEmailType FinalEmailToCreator { get; set; }
|
public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_RECEIVERS")]
|
[Column("FINAL_EMAIL_TO_RECEIVERS")]
|
||||||
public Constants.FinalEmailType FinalEmailToReceivers { get; set; }
|
public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("ADDED_WHEN", TypeName = "datetime")]
|
[Column("ADDED_WHEN", TypeName = "datetime")]
|
||||||
public DateTime AddedWhen { get; set; }
|
public DateTime AddedWhen { get; set; }
|
||||||
|
|
||||||
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
||||||
public DateTime ChangedWhen { get; set; }
|
public DateTime? ChangedWhen { get; set; }
|
||||||
|
|
||||||
[Column("EXPIRES_WARNING_DAYS")]
|
[Column("EXPIRES_WARNING_DAYS")]
|
||||||
public int ExpiresWarningDays { get; set; }
|
public int? ExpiresWarningDays { get; set; }
|
||||||
|
|
||||||
[Column("SEND_REMINDER_EMAILS")]
|
[Column("SEND_REMINDER_EMAILS")]
|
||||||
public bool SendReminderEmails { get; set; } = false;
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
[Column("FIRST_REMINDER_DAYS")]
|
[Column("FIRST_REMINDER_DAYS")]
|
||||||
public int FirstReminderDays { get; set; } = 0;
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
[Column("REMINDER_INTERVAL_DAYS")]
|
[Column("REMINDER_INTERVAL_DAYS")]
|
||||||
public int ReminderIntervalDays { get; set; } = 0;
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
[Column("CONTRACT_TYPE")]
|
[Column("CONTRACT_TYPE")]
|
||||||
public int ContractType { get; set; }
|
public int? ContractType { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool TFA_Enabled { get; set; } = false;
|
public bool TFA_Enabled { get; set; } = false;
|
||||||
@@ -70,14 +70,14 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int ExpiresWhenDays
|
public int ExpiresWhenDays
|
||||||
{
|
{
|
||||||
get => ExpiresDays;
|
get => ExpiresDays ?? 0;
|
||||||
set => ExpiresDays = value;
|
set => ExpiresDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int ExpiresWarningWhenDays
|
public int ExpiresWarningWhenDays
|
||||||
{
|
{
|
||||||
get => ExpiresWarningDays;
|
get => ExpiresWarningDays ?? 0;
|
||||||
set => ExpiresWarningDays = value;
|
set => ExpiresWarningDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,4 +86,4 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
return Title;
|
return Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
EnvelopeGenerator.Domain/Resources/Model.cs
Normal file
7
EnvelopeGenerator.Domain/Resources/Model.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace EnvelopeGenerator.Domain.Resources
|
||||||
|
{
|
||||||
|
// Marker type for IStringLocalizer lookup of Resources/Model.resx.
|
||||||
|
public class Model
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
116
EnvelopeGenerator.Domain/Resources/Model.fr-FR.resx
Normal file
116
EnvelopeGenerator.Domain/Resources/Model.fr-FR.resx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
|
||||||
|
<data name="AccessCodeIncorrect" xml:space="preserve">
|
||||||
|
<value>Code d'acces incorrect</value>
|
||||||
|
</data>
|
||||||
|
<data name="DocumentRejected" xml:space="preserve">
|
||||||
|
<value>Document rejete</value>
|
||||||
|
</data>
|
||||||
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
|
<value>Document signe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Draft" xml:space="preserve">
|
||||||
|
<value>Brouillon</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeCompletelySigned" xml:space="preserve">
|
||||||
|
<value>Entierement signe</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeCreated" xml:space="preserve">
|
||||||
|
<value>Enveloppe creee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeDeleted" xml:space="preserve">
|
||||||
|
<value>Enveloppe supprimee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopePartlySigned" xml:space="preserve">
|
||||||
|
<value>Partiellement signee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeQueued" xml:space="preserve">
|
||||||
|
<value>Enveloppe en file d'attente</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeRejected" xml:space="preserve">
|
||||||
|
<value>Enveloppe rejetee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeSaved" xml:space="preserve">
|
||||||
|
<value>Enregistree</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeSent" xml:space="preserve">
|
||||||
|
<value>Envoyee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeWithdrawn" xml:space="preserve">
|
||||||
|
<value>Enveloppe retiree</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageAccessCodeSent" xml:space="preserve">
|
||||||
|
<value>Code d'acces envoye</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageCompletionSent" xml:space="preserve">
|
||||||
|
<value>E-mail de cloture envoye</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageConfirmationSent" xml:space="preserve">
|
||||||
|
<value>Confirmation finale envoyee</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageDeletionSent" xml:space="preserve">
|
||||||
|
<value>Notification de suppression envoyee</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageInvitationSent" xml:space="preserve">
|
||||||
|
<value>Invitation envoyee</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure.Doc;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddDocInfrastructure(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IPdfPageImageRenderer, PdfPageImageRenderer>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DevExpress.Document.Processor" Version="25.2.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
57
EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
Normal file
57
EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using DevExpress.Drawing;
|
||||||
|
using DevExpress.Pdf;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure.Doc;
|
||||||
|
|
||||||
|
public class PdfPageImageRenderer : IPdfPageImageRenderer
|
||||||
|
{
|
||||||
|
private const float RenderDpi = 110f;
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(pdfBytes);
|
||||||
|
|
||||||
|
if (pdfBytes.Length == 0)
|
||||||
|
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
|
||||||
|
|
||||||
|
using var stream = new MemoryStream(pdfBytes, writable: false);
|
||||||
|
using var processor = new PdfDocumentProcessor();
|
||||||
|
processor.LoadDocument(stream);
|
||||||
|
|
||||||
|
var pages = processor.Document.Pages;
|
||||||
|
if (pages.Count == 0)
|
||||||
|
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
|
||||||
|
|
||||||
|
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(RenderDpi);
|
||||||
|
var results = new List<PdfPageImageDto>(pages.Count);
|
||||||
|
|
||||||
|
for (var i = 0; i < pages.Count; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var pageNumber = i + 1;
|
||||||
|
var page = pages[i];
|
||||||
|
var cropBox = page.CropBox;
|
||||||
|
|
||||||
|
using DXBitmap bitmap = processor.CreateDXBitmap(pageNumber, renderOptions);
|
||||||
|
using var imageStream = new MemoryStream();
|
||||||
|
bitmap.Save(imageStream, DXImageFormat.Png);
|
||||||
|
|
||||||
|
var dataUrl = $"data:image/png;base64,{Convert.ToBase64String(imageStream.ToArray())}";
|
||||||
|
|
||||||
|
results.Add(new PdfPageImageDto
|
||||||
|
{
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
WidthPt = cropBox.Width,
|
||||||
|
HeightPt = cropBox.Height,
|
||||||
|
WidthPx = bitmap.Width,
|
||||||
|
HeightPx = bitmap.Height,
|
||||||
|
ImageDataUrl = dataUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -214,4 +214,4 @@ namespace EnvelopeGenerator.Infrastructure
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#region Trigger
|
#region Trigger
|
||||||
// Configure entities to handle database triggers
|
// Configure entities to handle database triggers
|
||||||
void AddTrigger<T>() where T : class => _triggers
|
void AddTrigger<T>() where T : class
|
||||||
.Where(t => t.Key == typeof(T).Name)
|
{
|
||||||
.SelectMany(t => t.Value)
|
var entityName = typeof(T).Name;
|
||||||
.ForEach(tName =>
|
|
||||||
|
var configuredTriggerNames = _triggers
|
||||||
|
.Where(t => t.Key == entityName)
|
||||||
|
.SelectMany(t => t.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var triggerName in configuredTriggerNames)
|
||||||
{
|
{
|
||||||
#if NET
|
#if NET
|
||||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(tName));
|
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(triggerName));
|
||||||
#endif
|
#endif
|
||||||
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", tName, typeof(T).Name);
|
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", triggerName, entityName);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: call add trigger methods with attributes and reflection
|
// TODO: call add trigger methods with attributes and reflection
|
||||||
AddTrigger<Config>();
|
AddTrigger<Config>();
|
||||||
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Client.Components.PdfEditor;
|
||||||
|
|
||||||
|
public sealed record SenderPdfFieldMoveRequest(
|
||||||
|
int Index,
|
||||||
|
int Page,
|
||||||
|
double XPt,
|
||||||
|
double YPt);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Client.Components.PdfEditor;
|
||||||
|
|
||||||
|
public sealed record SenderPdfFieldOverlay(
|
||||||
|
int Index,
|
||||||
|
int Page,
|
||||||
|
double XPt,
|
||||||
|
double YPt,
|
||||||
|
string ReceiverName,
|
||||||
|
string Color);
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using EnvelopeGenerator.Application.Common.Dto
|
||||||
|
@using System.Globalization
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
<div class="sender-image-editor-shell">
|
||||||
|
@if (PageCount > 0)
|
||||||
|
{
|
||||||
|
<div class="sender-image-editor__toolbar-host">
|
||||||
|
<SenderPdfImageEditorToolbar CurrentPage="@_currentPage"
|
||||||
|
TotalPages="@PageCount"
|
||||||
|
ZoomPercent="@_zoomPercent"
|
||||||
|
MinZoomPercent="@MinZoomPercent"
|
||||||
|
MaxZoomPercent="@MaxZoomPercent"
|
||||||
|
ZoomStepPercent="@ZoomStepPercent"
|
||||||
|
GoFirstPage="OnGoFirstPageAsync"
|
||||||
|
GoPreviousPage="OnGoPreviousPageAsync"
|
||||||
|
GoNextPage="OnGoNextPageAsync"
|
||||||
|
GoLastPage="OnGoLastPageAsync"
|
||||||
|
GoToPage="OnGoToPageAsync"
|
||||||
|
ZoomOut="OnZoomOutAsync"
|
||||||
|
ZoomIn="OnZoomInAsync"
|
||||||
|
SetZoomPercent="OnSetZoomPercentAsync"
|
||||||
|
CanDownloadPdf="@CanDownloadPdf"
|
||||||
|
CanPrintPdf="@CanPrintPdf"
|
||||||
|
IsDownloading="@_isDownloading"
|
||||||
|
IsPrinting="@_isPrinting"
|
||||||
|
DownloadPdf="OnDownloadPdfAsync"
|
||||||
|
PrintPdf="OnPrintPdfAsync" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="pdf-editor-wrapper sender-image-editor sender-editor-pdf-viewer"
|
||||||
|
style="cursor: @(IsPlacementMode ? "crosshair" : (_isDragging ? "grabbing" : "default"));"
|
||||||
|
tabindex="0"
|
||||||
|
@onclick="HandleClickAsync"
|
||||||
|
@onwheel="OnEditorWheelAsync"
|
||||||
|
@onwheel:preventDefault="true"
|
||||||
|
@onscroll="OnEditorScrollAsync"
|
||||||
|
@onmousemove="OnEditorMouseMoveAsync"
|
||||||
|
@onmouseup="OnEditorMouseUp"
|
||||||
|
@onmouseleave="OnEditorMouseUp">
|
||||||
|
@if (PageCount <= 0)
|
||||||
|
{
|
||||||
|
<div class="sender-image-editor__empty">@L("SenderEditor_NoPagesToDisplay", "Keine Seiten zur Anzeige verfügbar.")</div>
|
||||||
|
}
|
||||||
|
else if (Pages.Count == 0 && !string.IsNullOrWhiteSpace(PdfDataUrl))
|
||||||
|
{
|
||||||
|
<div class="sender-image-editor__native-preview" style="@GetZoomStyle()">
|
||||||
|
<iframe class="sender-image-editor__native-frame"
|
||||||
|
src="@PdfDataUrl"
|
||||||
|
title="PDF Preview"></iframe>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="sender-image-editor__pages" style="@GetZoomStyle()">
|
||||||
|
@for (var pageIndex = 0; pageIndex < PageCount; pageIndex++)
|
||||||
|
{
|
||||||
|
var pagePreview = pageIndex < Pages.Count ? Pages[pageIndex] : null;
|
||||||
|
var pageNumber = pagePreview?.PageNumber ?? (pageIndex + 1);
|
||||||
|
var widthPt = pagePreview?.WidthPt ?? 595.0;
|
||||||
|
var heightPt = pagePreview?.HeightPt ?? 842.0;
|
||||||
|
|
||||||
|
<div class="sender-image-editor__page-block"
|
||||||
|
data-editor-page="@pageNumber"
|
||||||
|
data-page-width-pt="@widthPt"
|
||||||
|
data-page-height-pt="@heightPt">
|
||||||
|
<div class="sender-image-editor__page-canvas"
|
||||||
|
data-editor-page-canvas="@pageNumber"
|
||||||
|
style="@GetPageCanvasStyle(widthPt, heightPt)">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(pagePreview?.ImageDataUrl))
|
||||||
|
{
|
||||||
|
<img class="sender-image-editor__page-image"
|
||||||
|
src="@(pagePreview!.ImageDataUrl)"
|
||||||
|
alt="PDF Page @pageNumber"
|
||||||
|
draggable="false" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="sender-image-editor__page-placeholder">@L("SenderEditor_PageLabel", "Seite") @pageNumber</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var field in Fields.Where(f => f.Page == pageNumber))
|
||||||
|
{
|
||||||
|
<div class="sender-image-editor__field"
|
||||||
|
style="@GetFieldStyle(field, widthPt, heightPt)"
|
||||||
|
title="@field.ReceiverName"
|
||||||
|
@onmousedown="args => OnFieldMouseDownAsync(args, field)"
|
||||||
|
@onmousedown:preventDefault="true"
|
||||||
|
@onclick:stopPropagation="true">
|
||||||
|
<span class="sender-image-editor__field-label">@field.ReceiverName</span>
|
||||||
|
<button type="button"
|
||||||
|
class="sender-image-editor__field-remove"
|
||||||
|
title='@L("Delete", "Löschen")'
|
||||||
|
draggable="false"
|
||||||
|
@onclick:stopPropagation="true"
|
||||||
|
@onclick="() => RemoveField.InvokeAsync(field.Index)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public bool IsPlacementMode { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int PageCount { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<PdfPageImageDto> Pages { get; set; } = Array.Empty<PdfPageImageDto>();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? PdfDataUrl { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? PdfFileName { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<SenderPdfFieldOverlay> Fields { get; set; } = Array.Empty<SenderPdfFieldOverlay>();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<MouseEventArgs> EditorClick { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> RemoveField { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<SenderPdfFieldMoveRequest> MoveField { get; set; }
|
||||||
|
|
||||||
|
private int? _draggedFieldIndex;
|
||||||
|
private bool _isDragging;
|
||||||
|
private double _grabOffsetXPt;
|
||||||
|
private double _grabOffsetYPt;
|
||||||
|
private int _currentPage = 1;
|
||||||
|
private int _zoomPercent = 112;
|
||||||
|
private bool _zoomOverrideInitialized;
|
||||||
|
private bool _isDownloading;
|
||||||
|
private bool _isPrinting;
|
||||||
|
|
||||||
|
private const int MinZoomPercent = 50;
|
||||||
|
private const int MaxZoomPercent = 300;
|
||||||
|
private const int ZoomStepPercent = 5;
|
||||||
|
private bool CanDownloadPdf => !string.IsNullOrWhiteSpace(PdfDataUrl);
|
||||||
|
private bool CanPrintPdf => !string.IsNullOrWhiteSpace(PdfDataUrl);
|
||||||
|
|
||||||
|
private static string GetFieldStyle(SenderPdfFieldOverlay field, double pageWidthPt, double pageHeightPt)
|
||||||
|
{
|
||||||
|
var leftPercent = (field.XPt / pageWidthPt) * 100.0;
|
||||||
|
var topPercent = (field.YPt / pageHeightPt) * 100.0;
|
||||||
|
var widthPercent = (127.44 / pageWidthPt) * 100.0;
|
||||||
|
var heightPercent = (141.12 / pageHeightPt) * 100.0;
|
||||||
|
|
||||||
|
var left = leftPercent.ToString("F4", CultureInfo.InvariantCulture);
|
||||||
|
var top = topPercent.ToString("F4", CultureInfo.InvariantCulture);
|
||||||
|
var width = widthPercent.ToString("F4", CultureInfo.InvariantCulture);
|
||||||
|
var height = heightPercent.ToString("F4", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
return string.Join(' ',
|
||||||
|
"position:absolute;",
|
||||||
|
$"left:{left}%;",
|
||||||
|
$"top:{top}%;",
|
||||||
|
$"width:{width}%;",
|
||||||
|
$"height:{height}%;",
|
||||||
|
"border:2px solid;",
|
||||||
|
"border-radius:8px;",
|
||||||
|
"display:flex;",
|
||||||
|
"align-items:flex-start;",
|
||||||
|
"justify-content:space-between;",
|
||||||
|
"gap:0.35rem;",
|
||||||
|
"padding:0.2rem 0.3rem;",
|
||||||
|
"overflow:hidden;",
|
||||||
|
"z-index:30;",
|
||||||
|
$"border-color:{field.Color};",
|
||||||
|
$"background-color:{field.Color}22;");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPageCanvasStyle(double pageWidthPt, double pageHeightPt)
|
||||||
|
{
|
||||||
|
var ratio = pageHeightPt <= 0 ? 595.0 / 842.0 : pageWidthPt / pageHeightPt;
|
||||||
|
var ratioText = ratio.ToString("F6", CultureInfo.InvariantCulture);
|
||||||
|
var cursor = _isDragging
|
||||||
|
? "grabbing"
|
||||||
|
: (IsPlacementMode ? "crosshair" : "default");
|
||||||
|
return $"aspect-ratio:{ratioText};cursor:{cursor};";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetZoomStyle()
|
||||||
|
{
|
||||||
|
var factor = (_zoomPercent / 100.0).ToString("F4", CultureInfo.InvariantCulture);
|
||||||
|
return $"zoom:{factor};";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
if (PageCount <= 0)
|
||||||
|
{
|
||||||
|
_currentPage = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = Math.Max(1, Math.Min(_currentPage, PageCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (_zoomOverrideInitialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync(
|
||||||
|
"envelopeEditor.ensureZoomOverride",
|
||||||
|
"sender-editor-pdf-viewer");
|
||||||
|
|
||||||
|
_zoomOverrideInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleClickAsync(MouseEventArgs args)
|
||||||
|
{
|
||||||
|
if (EditorClick.HasDelegate)
|
||||||
|
await EditorClick.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFieldMouseDownAsync(MouseEventArgs args, SenderPdfFieldOverlay field)
|
||||||
|
{
|
||||||
|
var coords = await JSRuntime.InvokeAsync<EditorCoords?>(
|
||||||
|
"envelopeEditor.getClickCoordsOnPdfPage",
|
||||||
|
"sender-editor-pdf-viewer",
|
||||||
|
args.ClientX,
|
||||||
|
args.ClientY);
|
||||||
|
|
||||||
|
if (coords is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pageNumber = coords.PageNumber;
|
||||||
|
var (pageWidthPt, pageHeightPt) = GetPageSize(pageNumber);
|
||||||
|
var pointerXPt = coords.NormX * pageWidthPt;
|
||||||
|
var pointerYPt = coords.NormY * pageHeightPt;
|
||||||
|
|
||||||
|
_draggedFieldIndex = field.Index;
|
||||||
|
_isDragging = true;
|
||||||
|
_grabOffsetXPt = pointerXPt - field.XPt;
|
||||||
|
_grabOffsetYPt = pointerYPt - field.YPt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnEditorMouseMoveAsync(MouseEventArgs args)
|
||||||
|
{
|
||||||
|
if (_draggedFieldIndex is null || !MoveField.HasDelegate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var coords = await JSRuntime.InvokeAsync<EditorCoords?>(
|
||||||
|
"envelopeEditor.getClickCoordsOnPdfPage",
|
||||||
|
"sender-editor-pdf-viewer",
|
||||||
|
args.ClientX,
|
||||||
|
args.ClientY);
|
||||||
|
|
||||||
|
if (coords is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pageNumber = coords.PageNumber;
|
||||||
|
var (pageWidthPt, pageHeightPt) = GetPageSize(pageNumber);
|
||||||
|
|
||||||
|
const double sigWidthPt = 1.77 * 72;
|
||||||
|
const double sigHeightPt = 1.96 * 72;
|
||||||
|
|
||||||
|
var xPt = (coords.NormX * pageWidthPt) - _grabOffsetXPt;
|
||||||
|
var yPt = (coords.NormY * pageHeightPt) - _grabOffsetYPt;
|
||||||
|
xPt = Math.Max(0, Math.Min(xPt, pageWidthPt - sigWidthPt));
|
||||||
|
yPt = Math.Max(0, Math.Min(yPt, pageHeightPt - sigHeightPt));
|
||||||
|
|
||||||
|
await MoveField.InvokeAsync(new SenderPdfFieldMoveRequest(_draggedFieldIndex.Value, pageNumber, xPt, yPt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGoFirstPageAsync()
|
||||||
|
{
|
||||||
|
await NavigateToPageAsync(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGoPreviousPageAsync()
|
||||||
|
{
|
||||||
|
await NavigateToPageAsync(_currentPage - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGoNextPageAsync()
|
||||||
|
{
|
||||||
|
await NavigateToPageAsync(_currentPage + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGoLastPageAsync()
|
||||||
|
{
|
||||||
|
await NavigateToPageAsync(PageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGoToPageAsync(int page)
|
||||||
|
{
|
||||||
|
await NavigateToPageAsync(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NavigateToPageAsync(int targetPage)
|
||||||
|
{
|
||||||
|
if (PageCount <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var page = Math.Max(1, Math.Min(targetPage, PageCount));
|
||||||
|
_currentPage = page;
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync(
|
||||||
|
"envelopeEditor.scrollToPdfPage",
|
||||||
|
"sender-editor-pdf-viewer",
|
||||||
|
page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnEditorScrollAsync(EventArgs _)
|
||||||
|
{
|
||||||
|
if (PageCount <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var visiblePage = await JSRuntime.InvokeAsync<int?>(
|
||||||
|
"envelopeEditor.getVisiblePdfPageNumber",
|
||||||
|
"sender-editor-pdf-viewer");
|
||||||
|
|
||||||
|
if (!visiblePage.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var clamped = Math.Max(1, Math.Min(visiblePage.Value, PageCount));
|
||||||
|
if (clamped == _currentPage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_currentPage = clamped;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnZoomOutAsync()
|
||||||
|
{
|
||||||
|
SetZoomPercent(_zoomPercent - ZoomStepPercent);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnZoomInAsync()
|
||||||
|
{
|
||||||
|
SetZoomPercent(_zoomPercent + ZoomStepPercent);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSetZoomPercentAsync(int zoomPercent)
|
||||||
|
{
|
||||||
|
SetZoomPercent(zoomPercent);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnEditorWheelAsync(WheelEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.CtrlKey)
|
||||||
|
{
|
||||||
|
if (args.DeltaY < 0)
|
||||||
|
await OnZoomInAsync();
|
||||||
|
else if (args.DeltaY > 0)
|
||||||
|
await OnZoomOutAsync();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync(
|
||||||
|
"envelopeEditor.scrollPdfViewerByDelta",
|
||||||
|
"sender-editor-pdf-viewer",
|
||||||
|
args.DeltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetZoomPercent(int value)
|
||||||
|
{
|
||||||
|
_zoomPercent = Math.Max(MinZoomPercent, Math.Min(value, MaxZoomPercent));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnDownloadPdfAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(PdfDataUrl) || _isDownloading || _isPrinting)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDownloading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fallbackName = string.IsNullOrWhiteSpace(PdfFileName)
|
||||||
|
? "document.pdf"
|
||||||
|
: PdfFileName;
|
||||||
|
|
||||||
|
var fileName = fallbackName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? fallbackName
|
||||||
|
: $"{fallbackName}.pdf";
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync(
|
||||||
|
"envelopeEditor.downloadDataUrlAsFile",
|
||||||
|
PdfDataUrl,
|
||||||
|
fileName,
|
||||||
|
"application/pdf");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isDownloading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnPrintPdfAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(PdfDataUrl) || _isPrinting || _isDownloading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isPrinting = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync(
|
||||||
|
"envelopeEditor.printDataUrlPdf",
|
||||||
|
PdfDataUrl,
|
||||||
|
"application/pdf");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isPrinting = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEditorMouseUp(MouseEventArgs args)
|
||||||
|
{
|
||||||
|
_draggedFieldIndex = null;
|
||||||
|
_isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (double WidthPt, double HeightPt) GetPageSize(int pageNumber)
|
||||||
|
{
|
||||||
|
var page = Pages.FirstOrDefault(p => p.PageNumber == pageNumber);
|
||||||
|
return page is null ? (595.0, 842.0) : (page.WidthPt, page.HeightPt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record EditorCoords(double NormX, double NormY, int PageIndex, int PageNumber);
|
||||||
|
|
||||||
|
private string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<div class="sender-image-toolbar" @onclick:stopPropagation="true">
|
||||||
|
<div class="sender-image-toolbar__group sender-image-toolbar__group--nav" role="group" aria-label="Page navigation">
|
||||||
|
<button type="button" class="sender-image-toolbar__icon-btn" title="First page" disabled="@(!CanGoPrevious)" @onclick="GoToFirstAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M8 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M16 7L11 12L16 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="sender-image-toolbar__icon-btn" title="Previous page" disabled="@(!CanGoPrevious)" @onclick="GoToPreviousAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M15 7L10 12L15 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="sender-image-toolbar__value-box">
|
||||||
|
<input type="number"
|
||||||
|
min="1"
|
||||||
|
max="@TotalPagesDisplay"
|
||||||
|
value="@CurrentPageDisplay"
|
||||||
|
class="sender-image-toolbar__page-input"
|
||||||
|
@onchange="OnPageInputChangedAsync"
|
||||||
|
disabled="@(TotalPagesDisplay <= 0)" />
|
||||||
|
<span class="sender-image-toolbar__page-total">of @TotalPagesDisplay</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-image-toolbar__select-wrap" title="Page list">
|
||||||
|
<button type="button" class="sender-image-toolbar__icon-btn sender-image-toolbar__icon-btn--caret" disabled="@(TotalPagesDisplay <= 0)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<select class="sender-image-toolbar__page-select"
|
||||||
|
aria-label="Select page"
|
||||||
|
disabled="@(TotalPagesDisplay <= 0)"
|
||||||
|
value="@CurrentPageDisplay"
|
||||||
|
@onchange="OnPageSelectChangedAsync">
|
||||||
|
@for (var pageNumber = 1; pageNumber <= TotalPagesDisplay; pageNumber++)
|
||||||
|
{
|
||||||
|
<option value="@pageNumber">@pageNumber</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="sender-image-toolbar__icon-btn" title="Next page" disabled="@(!CanGoNext)" @onclick="GoToNextAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M9 7L14 12L9 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="sender-image-toolbar__icon-btn" title="Last page" disabled="@(!CanGoNext)" @onclick="GoToLastAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M16 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M8 7L13 12L8 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-image-toolbar__group sender-image-toolbar__group--zoom" role="group" aria-label="Zoom controls">
|
||||||
|
<button type="button" class="sender-image-toolbar__icon-btn" title="Zoom out" disabled="@(!CanZoomOut)" @onclick="ZoomOutAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M6 12H18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="sender-image-toolbar__value-box" aria-hidden="true">
|
||||||
|
<span>@ZoomPercentDisplay%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="range"
|
||||||
|
class="sender-image-toolbar__zoom-slider"
|
||||||
|
min="@MinZoomPercent"
|
||||||
|
max="@MaxZoomPercent"
|
||||||
|
step="@ZoomStepPercent"
|
||||||
|
value="@ZoomPercentDisplay"
|
||||||
|
@oninput="OnZoomSliderInputAsync"
|
||||||
|
aria-label="Zoom slider" />
|
||||||
|
|
||||||
|
<button type="button" class="sender-image-toolbar__icon-btn" title="Zoom in" disabled="@(!CanZoomIn)" @onclick="ZoomInAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M12 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M6 12H18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-image-toolbar__group sender-image-toolbar__group--actions" role="group" aria-label="Document actions">
|
||||||
|
<button type="button"
|
||||||
|
class="sender-image-toolbar__icon-btn @(IsPrinting ? "sender-image-toolbar__icon-btn--busy" : string.Empty)"
|
||||||
|
title="@(IsPrinting ? "Preparing print..." : "Print")"
|
||||||
|
disabled="@(!CanPrintPdf || IsPrinting || IsDownloading)"
|
||||||
|
@onclick="PrintPdfAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M7 9V4H17V9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect x="5" y="9" width="14" height="8" rx="2" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M7 14H17V20H7V14Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="sender-image-toolbar__icon-btn @(IsDownloading ? "sender-image-toolbar__icon-btn--busy" : string.Empty)"
|
||||||
|
title="@(IsDownloading ? "Preparing download..." : "Download PDF")"
|
||||||
|
disabled="@(!CanDownloadPdf || IsDownloading || IsPrinting)"
|
||||||
|
@onclick="DownloadPdfAsync">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M12 4V14" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M8 10L12 14L16 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M5 18H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public int CurrentPage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int ZoomPercent { get; set; } = 112;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int MinZoomPercent { get; set; } = 50;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int MaxZoomPercent { get; set; } = 300;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int ZoomStepPercent { get; set; } = 5;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback GoFirstPage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback GoPreviousPage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback GoNextPage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback GoLastPage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> GoToPage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback ZoomOut { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback ZoomIn { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> SetZoomPercent { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool CanDownloadPdf { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool CanPrintPdf { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsDownloading { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsPrinting { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback DownloadPdf { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback PrintPdf { get; set; }
|
||||||
|
|
||||||
|
private int CurrentPageDisplay => CurrentPage > 0 ? CurrentPage : 0;
|
||||||
|
private int TotalPagesDisplay => TotalPages > 0 ? TotalPages : 0;
|
||||||
|
private int ZoomPercentDisplay => ZoomPercent > 0 ? ZoomPercent : 100;
|
||||||
|
private bool CanGoPrevious => CurrentPageDisplay > 1;
|
||||||
|
private bool CanGoNext => TotalPagesDisplay > 0 && CurrentPageDisplay < TotalPagesDisplay;
|
||||||
|
private bool CanZoomOut => ZoomPercentDisplay > MinZoomPercent;
|
||||||
|
private bool CanZoomIn => ZoomPercentDisplay < MaxZoomPercent;
|
||||||
|
|
||||||
|
private async Task GoToFirstAsync()
|
||||||
|
{
|
||||||
|
if (GoFirstPage.HasDelegate)
|
||||||
|
await GoFirstPage.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GoToPreviousAsync()
|
||||||
|
{
|
||||||
|
if (GoPreviousPage.HasDelegate)
|
||||||
|
await GoPreviousPage.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GoToNextAsync()
|
||||||
|
{
|
||||||
|
if (GoNextPage.HasDelegate)
|
||||||
|
await GoNextPage.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GoToLastAsync()
|
||||||
|
{
|
||||||
|
if (GoLastPage.HasDelegate)
|
||||||
|
await GoLastPage.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnPageInputChangedAsync(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
if (!GoToPage.HasDelegate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!int.TryParse(args.Value?.ToString(), out var page))
|
||||||
|
return;
|
||||||
|
|
||||||
|
page = Math.Max(1, Math.Min(page, Math.Max(1, TotalPagesDisplay)));
|
||||||
|
await GoToPage.InvokeAsync(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnPageSelectChangedAsync(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
if (!GoToPage.HasDelegate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!int.TryParse(args.Value?.ToString(), out var page))
|
||||||
|
return;
|
||||||
|
|
||||||
|
page = Math.Max(1, Math.Min(page, Math.Max(1, TotalPagesDisplay)));
|
||||||
|
await GoToPage.InvokeAsync(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ZoomOutAsync()
|
||||||
|
{
|
||||||
|
if (ZoomOut.HasDelegate)
|
||||||
|
await ZoomOut.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ZoomInAsync()
|
||||||
|
{
|
||||||
|
if (ZoomIn.HasDelegate)
|
||||||
|
await ZoomIn.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnZoomSliderInputAsync(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
if (!SetZoomPercent.HasDelegate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!int.TryParse(args.Value?.ToString(), out var zoomPercent))
|
||||||
|
return;
|
||||||
|
|
||||||
|
zoomPercent = Math.Max(MinZoomPercent, Math.Min(zoomPercent, MaxZoomPercent));
|
||||||
|
await SetZoomPercent.InvokeAsync(zoomPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DownloadPdfAsync()
|
||||||
|
{
|
||||||
|
if (DownloadPdf.HasDelegate)
|
||||||
|
await DownloadPdf.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PrintPdfAsync()
|
||||||
|
{
|
||||||
|
if (PrintPdf.HasDelegate)
|
||||||
|
await PrintPdf.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
|
||||||
|
@inject LocalizationService LocalizationService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
|
||||||
|
<div class="sf-lang-combo">
|
||||||
|
<button type="button"
|
||||||
|
class="sf-lang-trigger"
|
||||||
|
@onclick="ToggleMenu"
|
||||||
|
aria-label="@L("Language", "Language")"
|
||||||
|
aria-expanded="@_isOpen">
|
||||||
|
<span class="fi @_selected.FlagClass"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (_isOpen)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="sf-lang-backdrop"
|
||||||
|
aria-label="@L("CloseLanguageMenu", "Close language menu")"
|
||||||
|
@onclick="CloseMenu">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="sf-lang-menu" role="listbox" aria-label="@L("Languages", "Languages")">
|
||||||
|
@foreach (var option in _options)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="sf-lang-option @(_selected.Code == option.Code ? "sf-lang-option--active" : string.Empty)"
|
||||||
|
role="option"
|
||||||
|
aria-selected="@(_selected.Code == option.Code)"
|
||||||
|
@onmousedown:preventDefault="true"
|
||||||
|
@onclick="() => SelectLanguageAsync(option)">
|
||||||
|
<span class="fi @option.FlagClass"></span>
|
||||||
|
<span>@option.ShortName</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private readonly IReadOnlyList<LanguageOption> _options =
|
||||||
|
[
|
||||||
|
new("de-DE", "fi-de", "DE"),
|
||||||
|
new("en-US", "fi-us", "EN"),
|
||||||
|
new("fr-FR", "fi-fr", "FR")
|
||||||
|
];
|
||||||
|
|
||||||
|
private LanguageOption _selected = new("de-DE", "fi-de", "DE");
|
||||||
|
private bool _isOpen;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var currentLanguage = await LocalizationService.GetLanguageAsync();
|
||||||
|
_selected = _options.FirstOrDefault(x => x.Code.Equals(currentLanguage, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? _options[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleMenu()
|
||||||
|
{
|
||||||
|
_isOpen = !_isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseMenu()
|
||||||
|
{
|
||||||
|
_isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SelectLanguageAsync(LanguageOption option)
|
||||||
|
{
|
||||||
|
if (_selected.Code.Equals(option.Code, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_isOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selected = option;
|
||||||
|
_isOpen = false;
|
||||||
|
await LocalizationService.SetLanguageAsync(option.Code);
|
||||||
|
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
|
||||||
|
|
||||||
|
string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using System.Globalization
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@inject ThemeService ThemeService
|
@inject ThemeService ThemeService
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<HeadContent>
|
<HeadContent>
|
||||||
<link href="@ThemeService.ThemeCssHref" rel="stylesheet" />
|
<link href="@ThemeService.ThemeCssHref" rel="stylesheet" />
|
||||||
@@ -18,10 +24,13 @@
|
|||||||
<div class="receiver-footer__left">
|
<div class="receiver-footer__left">
|
||||||
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||||
<span class="receiver-footer__sep">|</span>
|
<span class="receiver-footer__sep">|</span>
|
||||||
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
<a href="@GetPrivacyPolicyPath()" target="_blank" rel="noopener">@L("PrivacyPolicy", "Datenschutz")</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ThemeControls />
|
<div class="receiver-footer__right">
|
||||||
|
<LanguageControls />
|
||||||
|
<ThemeControls />
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -32,6 +41,7 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
Navigation.LocationChanged += OnLocationChanged;
|
||||||
await FontLoader.LoadFonts(HttpClientFactory, ["opensans.ttf"]);
|
await FontLoader.LoadFonts(HttpClientFactory, ["opensans.ttf"]);
|
||||||
_themeChangeHandler = () => InvokeAsync(StateHasChanged);
|
_themeChangeHandler = () => InvokeAsync(StateHasChanged);
|
||||||
ThemeService.OnChange += _themeChangeHandler;
|
ThemeService.OnChange += _themeChangeHandler;
|
||||||
@@ -39,5 +49,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> ThemeService.OnChange -= _themeChangeHandler;
|
{
|
||||||
|
ThemeService.OnChange -= _themeChangeHandler;
|
||||||
|
Navigation.LocationChanged -= OnLocationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||||
|
=> _ = InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
private string GetPrivacyPolicyPath()
|
||||||
|
{
|
||||||
|
var current = CultureInfo.CurrentUICulture.Name;
|
||||||
|
var code = current switch
|
||||||
|
{
|
||||||
|
"de-DE" => "de-DE",
|
||||||
|
"en-US" => "en-US",
|
||||||
|
"fr-FR" => "fr-FR",
|
||||||
|
_ => "de-DE"
|
||||||
|
};
|
||||||
|
|
||||||
|
return $"docs/privacy-policy.{code}.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
|
public sealed record SignatureSubmitDto
|
||||||
|
{
|
||||||
|
public required int Id { get; init; }
|
||||||
|
public required string DataUrl { get; init; }
|
||||||
|
public required string FullName { get; init; }
|
||||||
|
public string? Position { get; init; }
|
||||||
|
public required string Place { get; init; }
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
@page "/envelope/signed-result"
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
|
||||||
|
<div class="receiver-signed-result-page">
|
||||||
|
<div class="receiver-signed-result-card">
|
||||||
|
<div class="receiver-signed-result-icon" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 8a5.5 5.5 0 0 1 8.25-4.764.5.5 0 0 0 .5-.866A6.5 6.5 0 1 0 14.5 8a.5.5 0 0 0-1 0 5.5 5.5 0 1 1-11 0z" />
|
||||||
|
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="receiver-signed-result-title">@L("DocumentSuccessfullySigned", "Dokument erfolgreich unterzeichnet")</h1>
|
||||||
|
<p class="receiver-signed-result-message">
|
||||||
|
@L("DocumentSignedConfirmationMessage", "Vielen Dank. Ihre Unterschrift wurde erfolgreich gespeichert und an die beteiligten Parteien übermittelt.")
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveWebAssembly
|
||||||
|
|
||||||
<div class="home-page-wrapper">
|
<div class="home-page-wrapper">
|
||||||
@@ -24,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<h1 class="home-hero-header__title">SignFlow</h1>
|
<h1 class="home-hero-header__title">SignFlow</h1>
|
||||||
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
|
<p class="home-hero-header__subtitle">@L("Index_Subtitle", "Willkommen im eSign-Portal")</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Zur Umschlag-Übersicht
|
@L("Index_ToEnvelopeOverview", "Zur Umschlag-Übersicht")
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -54,20 +57,20 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Sicherer Zugang
|
@L("Index_SecureAccess", "Sicherer Zugang")
|
||||||
</div>
|
</div>
|
||||||
<div class="home-feature-badge">
|
<div class="home-feature-badge">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Digitale Unterschrift
|
@L("Index_DigitalSignature", "Digitale Unterschrift")
|
||||||
</div>
|
</div>
|
||||||
<div class="home-feature-badge">
|
<div class="home-feature-badge">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
PDF-Export
|
@L("Index_PdfExport", "PDF-Export")
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -80,10 +83,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private const string HomePageDescription =
|
private string HomePageDescription => L(
|
||||||
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. " +
|
"HomePageDescription",
|
||||||
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.");
|
||||||
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
|
||||||
|
|
||||||
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
|
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
|
||||||
|
|
||||||
@@ -99,4 +101,10 @@
|
|||||||
if (!firstRender) return;
|
if (!firstRender) return;
|
||||||
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
@page "/envelope/login/{EnvelopeKey}"
|
@page "/envelope/login/{EnvelopeKey}"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveWebAssembly
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
|
||||||
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
||||||
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
||||||
@@ -13,14 +16,14 @@
|
|||||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
<h5 class="mb-0 fw-semibold">@L("OpenDocument", "Dokument öffnen")</h5>
|
||||||
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
|
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">@L("ReceiverLogin_Subtitle", "Sicherer Zugang mit Zugangscode")</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
|
|
||||||
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
||||||
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
@L("ReceiverLogin_Description", "Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.")
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (LoginResult == EnvelopeLoginResult.NotFound)
|
@if (LoginResult == EnvelopeLoginResult.NotFound)
|
||||||
@@ -30,8 +33,8 @@
|
|||||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Dokument nicht gefunden.</strong><br />
|
<strong>@L("ReceiverLogin_NotFound_Title", "Dokument nicht gefunden.")</strong><br />
|
||||||
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
<span style="font-size:0.85rem;">@L("ReceiverLogin_NotFound_Body", "Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.")</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -42,8 +45,8 @@
|
|||||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Ungültiger Zugangscode.</strong><br />
|
<strong>@L("ReceiverLogin_InvalidCode_Title", "Ungültiger Zugangscode.")</strong><br />
|
||||||
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
<span style="font-size:0.85rem;">@L("ReceiverLogin_InvalidCode_Body", "Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.")</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -55,15 +58,15 @@
|
|||||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Serverfehler.</strong><br />
|
<strong>@L("ServerError", "Serverfehler.")</strong><br />
|
||||||
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
<span style="font-size:0.85rem;">@L("UnexpectedErrorTryAgainLater", "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.")</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label fw-medium" for="login-access-code">
|
<label class="form-label fw-medium" for="login-access-code">
|
||||||
Zugangscode
|
@L("AccessCode", "Zugangscode")
|
||||||
<span class="text-danger ms-1">*</span>
|
<span class="text-danger ms-1">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -75,7 +78,7 @@
|
|||||||
<input id="login-access-code"
|
<input id="login-access-code"
|
||||||
type="@(ShowCode ? "text" : "password")"
|
type="@(ShowCode ? "text" : "password")"
|
||||||
class="form-control border-start-0 border-end-0 @(LoginResult == EnvelopeLoginResult.InvalidCode ? "is-invalid" : null)"
|
class="form-control border-start-0 border-end-0 @(LoginResult == EnvelopeLoginResult.InvalidCode ? "is-invalid" : null)"
|
||||||
placeholder="Zugangscode eingeben"
|
placeholder="@L("ReceiverLogin_AccessCode_Placeholder", "Zugangscode eingeben")"
|
||||||
@bind="AccessCode"
|
@bind="AccessCode"
|
||||||
@bind:event="oninput"
|
@bind:event="oninput"
|
||||||
@onkeydown="OnKeyDownAsync"
|
@onkeydown="OnKeyDownAsync"
|
||||||
@@ -113,21 +116,21 @@
|
|||||||
@if (IsLoading)
|
@if (IsLoading)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
<span>Überprüfen …</span>
|
<span>@L("VerifyInProgress", "Überprüfen …")</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
||||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Dokument öffnen</span>
|
<span>@L("OpenDocument", "Dokument öffnen")</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
|
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
|
||||||
Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.
|
@L("ReceiverLogin_FooterHelp", "Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.")
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -141,6 +144,14 @@
|
|||||||
bool IsLoading;
|
bool IsLoading;
|
||||||
EnvelopeLoginResult? LoginResult;
|
EnvelopeLoginResult? LoginResult;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(EnvelopeKey))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = await AuthService.RequestEnvelopeReceiverAccessCodeAsync(EnvelopeKey);
|
||||||
|
}
|
||||||
|
|
||||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
|
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == "Enter")
|
if (e.Key == "Enter")
|
||||||
@@ -159,7 +170,7 @@
|
|||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success)
|
if (result == EnvelopeLoginResult.Success)
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,5 +178,11 @@
|
|||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
@page "/sender/login"
|
@page "/sender/login"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveWebAssembly
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
|
||||||
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
||||||
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
||||||
@@ -14,14 +17,14 @@
|
|||||||
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
|
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="mb-0 fw-semibold">Sender Anmeldung</h5>
|
<h5 class="mb-0 fw-semibold">@L("SenderLogin_Title", "Sender Anmeldung")</h5>
|
||||||
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang zum Sender-Dashboard</p>
|
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">@L("SenderLogin_Subtitle", "Sicherer Zugang zum Sender-Dashboard")</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
|
|
||||||
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
||||||
Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.
|
@L("SenderLogin_Description", "Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.")
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (LoginResult == SenderLoginResult.InvalidCredentials) {
|
@if (LoginResult == SenderLoginResult.InvalidCredentials) {
|
||||||
@@ -30,8 +33,8 @@
|
|||||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Ungültige Anmeldedaten.</strong><br />
|
<strong>@L("SenderLogin_InvalidCredentials_Title", "Ungültige Anmeldedaten.")</strong><br />
|
||||||
<span style="font-size:0.85rem;">Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</span>
|
<span style="font-size:0.85rem;">@L("SenderLogin_InvalidCredentials_Body", "Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.")</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else if (LoginResult == SenderLoginResult.Error) {
|
} else if (LoginResult == SenderLoginResult.Error) {
|
||||||
@@ -41,15 +44,15 @@
|
|||||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Serverfehler.</strong><br />
|
<strong>@L("ServerError", "Serverfehler.")</strong><br />
|
||||||
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
<span style="font-size:0.85rem;">@L("UnexpectedErrorTryAgainLater", "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.")</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-medium" for="login-username">
|
<label class="form-label fw-medium" for="login-username">
|
||||||
Benutzername
|
@L("UserName", "Benutzername")
|
||||||
<span class="text-danger ms-1">*</span>
|
<span class="text-danger ms-1">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -61,7 +64,7 @@
|
|||||||
<input id="login-username"
|
<input id="login-username"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
class="form-control @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
||||||
placeholder="Benutzername eingeben"
|
placeholder="@L("SenderLogin_Username_Placeholder", "Benutzername eingeben")"
|
||||||
@bind="Username"
|
@bind="Username"
|
||||||
@bind:event="oninput"
|
@bind:event="oninput"
|
||||||
@onkeydown="OnKeyDownAsync"
|
@onkeydown="OnKeyDownAsync"
|
||||||
@@ -72,7 +75,7 @@
|
|||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label fw-medium" for="login-password">
|
<label class="form-label fw-medium" for="login-password">
|
||||||
Passwort
|
@L("Password", "Passwort")
|
||||||
<span class="text-danger ms-1">*</span>
|
<span class="text-danger ms-1">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -84,7 +87,7 @@
|
|||||||
<input id="login-password"
|
<input id="login-password"
|
||||||
type="@(ShowPassword ? "text" : "password")"
|
type="@(ShowPassword ? "text" : "password")"
|
||||||
class="form-control border-start-0 border-end-0 @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
class="form-control border-start-0 border-end-0 @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
||||||
placeholder="Passwort eingeben"
|
placeholder="@L("SenderLogin_Password_Placeholder", "Passwort eingeben")"
|
||||||
@bind="Password"
|
@bind="Password"
|
||||||
@bind:event="oninput"
|
@bind:event="oninput"
|
||||||
@onkeydown="OnKeyDownAsync"
|
@onkeydown="OnKeyDownAsync"
|
||||||
@@ -118,20 +121,20 @@
|
|||||||
disabled="@(IsLoading || string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))">
|
disabled="@(IsLoading || string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))">
|
||||||
@if (IsLoading) {
|
@if (IsLoading) {
|
||||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
<span>Anmelden …</span>
|
<span>@L("LoginInProgress", "Anmelden …")</span>
|
||||||
} else {
|
} else {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0v-2z"/>
|
<path fill-rule="evenodd" d="M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0v-2z"/>
|
||||||
<path fill-rule="evenodd" d="M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3z"/>
|
<path fill-rule="evenodd" d="M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Anmelden</span>
|
<span>@L("Login", "Anmelden")</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
|
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
|
||||||
Bei Problemen wenden Sie sich bitte an den Administrator.
|
@L("SenderLogin_FooterHelp", "Bei Problemen wenden Sie sich bitte an den Administrator.")
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +162,7 @@
|
|||||||
var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim());
|
var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim());
|
||||||
|
|
||||||
if (result == SenderLoginResult.Success) {
|
if (result == SenderLoginResult.Success) {
|
||||||
Navigation.NavigateTo("/sender", forceLoad: true);
|
Navigation.NavigateTo("/sender", forceLoad: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,4 +170,10 @@
|
|||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user