Compare commits
68 Commits
bugfix/leg
...
42a853cb0d
| Author | SHA1 | Date | |
|---|---|---|---|
| 42a853cb0d | |||
| b46c3c8ebe | |||
| 9f712a6b8c | |||
| 4449743105 | |||
| 435be882c9 | |||
| 09257d7766 | |||
| 16fa4b460f | |||
| f4aae8c787 | |||
| ed1468194d | |||
| f9f94c900e | |||
| 18df6221c9 | |||
| b15f95755c | |||
| 4be9e38a9e | |||
| 11c8886d6f | |||
| 1ad673904a | |||
| 32a9d008a4 | |||
| 454dd0b6cc | |||
| aa2a31e993 | |||
| ecc509919c | |||
| f3751274b1 | |||
| adc231f10e | |||
| 999fac7dac | |||
| 86758bb127 | |||
| 498d59a4ce | |||
| ce430c1b80 | |||
| eb219291de | |||
| 80733ab1f7 | |||
| 3d9153da23 | |||
| a0bfd405f0 | |||
| ec3a15a467 | |||
| 80f08b393c | |||
| b22d57d7da | |||
| 01cd8b2607 | |||
| 310d2efc6e | |||
| e5402bafc9 | |||
| f517ab4cfb | |||
| 6d63f9db2b | |||
| 46c7c31e03 | |||
| 8f6db435bb | |||
| 9bf7676464 | |||
| d9556c18ca | |||
| 5b7157f422 | |||
| 119ee3ec16 | |||
| b536b31a4b | |||
| a80efee075 | |||
| 86d6daafbd | |||
| aefa1a8883 | |||
| b3e7f77165 | |||
| bac9bfd036 | |||
| 6effb8a0d6 | |||
| a9ff0d88f2 | |||
| 0fdfe991ea | |||
| 7fc2c14f9c | |||
| 5098deedd1 | |||
| 2e634361c5 | |||
| 60ebebc0d8 | |||
| c2d8b6531c | |||
| 665266457f | |||
| 14d581acac | |||
| eb533a1dc2 | |||
| 36025fad7b | |||
| 0606efabf9 | |||
| 2cb1e266f7 | |||
| 35c1374b9c | |||
| feaea1626b | |||
| babd96200a | |||
| f9cfcf1f19 | |||
| ffb9e07466 |
107
AGENTS.md
107
AGENTS.md
@@ -4,19 +4,78 @@
|
||||
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
||||
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
|
||||
|
||||
---
|
||||
|
||||
## Documentation Catalog
|
||||
|
||||
This section explains every markdown file in the repo: what it contains, why it exists, and when you need it. Read this before deciding which file to open.
|
||||
|
||||
### Core Architecture Docs (Active — Always Relevant)
|
||||
|
||||
#### `AGENTS.md` ← you are here
|
||||
**What:** Authoritative agent/copilot onboarding guide. Covers project structure, route table, render mode rules, coordinate system, API gaps, and common mistakes.
|
||||
**Why it exists:** Single source of truth for agents so they don't re-derive architecture from code on every session.
|
||||
**When you need it:** Always. Read first on every session.
|
||||
**Maintenance:** Update whenever project structure, routes, render modes, or naming conventions change.
|
||||
|
||||
#### `COPILOT_CONTEXT.md`
|
||||
**What:** Deep-dive architecture reference. Covers the full Blazor Auto hybrid setup, authentication flows (JWT + cookies, per-envelope receiver tokens), service registration patterns, coordinate system proof, and an explicit "prefer Server/Server.Client over old WebUI/ReceiverUI" rule.
|
||||
**Why it exists:** Prevents agents from re-deriving or misinterpreting the hybrid SSR+WASM architecture. Also documents coordinate system evidence to prevent regressions (inches vs. points vs. pixels).
|
||||
**When you need it:**
|
||||
- Implementing any new page or service
|
||||
- Debugging auth/cookie issues
|
||||
- Any coordinate conversion question
|
||||
- Unsure which project a new file should go in
|
||||
**Maintenance:** Update when render mode assignments change, new auth schemes are added, or the coordinate system is touched.
|
||||
|
||||
#### `FORM_APPLICATION_CONTEXT.md`
|
||||
**What:** Comprehensive reverse-engineering of the legacy VB.NET WinForms app (`EnvelopeGenerator.Form`). Documents every form (frmMain, frmEnvelopeEditor, frmFieldEditor, etc.), their UI layout, toolbar actions, data models, coordinate system, and a feature-by-feature migration mapping table to `EnvelopeGenerator.Server`.
|
||||
**Why it exists:** The WinForms app is the functional specification for the sender-side web UI. It documents the full intended workflow that is being migrated to Blazor — especially the envelope creation, signature field placement, and dashboard features that are not yet implemented.
|
||||
**When you need it:**
|
||||
- Implementing any sender-side feature (`/sender`, `/sender/editor`, `/sender/envelope/{id}`)
|
||||
- Understanding what a specific toolbar button or form should do
|
||||
- Designing API endpoints for sender workflow
|
||||
- Checking coordinate system behavior (INCHES proof is here)
|
||||
**Maintenance:** Update the mapping table as features are implemented (change `❌ Not implemented` → `✅ Exists`). Do not rename `ReceiverUI` back — it was the old project name; current target is `EnvelopeGenerator.Server`.
|
||||
|
||||
#### `RECEIVER_PDF_VIEWER_CONTEXT.md`
|
||||
**What:** Detailed technical reference for the receiver-side PDF viewer (`EnvelopeReceiverPage.razor`). Documents the PDF.js integration, signature button rendering, JS interop API (`window.pdfViewer`), signature capture flow, HiDPI/zoom/thumbnail config, and the full component lifecycle.
|
||||
**Why it exists:** The receiver PDF viewer is the most complex component in the codebase. This doc prevents agents from misunderstanding the JS↔Blazor boundary, re-implementing already-solved problems (HiDPI, zoom, signature caching), or breaking the signing flow.
|
||||
**When you need it:**
|
||||
- Working on `EnvelopeReceiverPage.razor` or `pdf-viewer.js`
|
||||
- Adding new signature field rendering logic
|
||||
- Debugging signature rendering, zoom, or thumbnail issues
|
||||
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
|
||||
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
|
||||
|
||||
---
|
||||
|
||||
### Deployment Docs
|
||||
|
||||
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
||||
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
|
||||
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
|
||||
**When you need it:**
|
||||
- Deploying to IIS for the first time
|
||||
- Troubleshooting 500 errors on production after deploy
|
||||
- Setting up a new deployment environment
|
||||
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
|
||||
|
||||
---
|
||||
|
||||
## Active Architecture (Post-Migration)
|
||||
|
||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||
- **WebUI** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||
- **WebUI.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||
|
||||
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0)
|
||||
|
||||
**Proxy:** YARP in WebUI routes `/api/*` → `localhost:8088` (API)
|
||||
**Proxy:** YARP in EnvelopeGenerator.Server routes `/api/*` → `localhost:8088` (API)
|
||||
|
||||
### Deprecated Projects - DO NOT USE
|
||||
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to WebUI)
|
||||
- `EnvelopeGenerator.Web` - Razor Pages (replaced by WebUI)
|
||||
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
||||
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
|
||||
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
||||
|
||||
## Development Commands
|
||||
@@ -28,11 +87,11 @@ cd EnvelopeGenerator.API
|
||||
dotnet run
|
||||
|
||||
# Terminal 2 - Blazor Frontend
|
||||
cd EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI
|
||||
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||
dotnet run
|
||||
```
|
||||
|
||||
**Critical:** Both must run simultaneously. WebUI proxy forwards `/api/*` to API.
|
||||
**Critical:** Both must run simultaneously. EnvelopeGenerator.Server proxy forwards `/api/*` to API.
|
||||
|
||||
### Build
|
||||
```powershell
|
||||
@@ -46,23 +105,25 @@ EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc
|
||||
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
||||
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
||||
EnvelopeGenerator.API/ # Controllers, endpoints
|
||||
EnvelopeGenerator.WebUI/ # Server-side Blazor components
|
||||
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||
EnvelopeGenerator.WebUI.Client/ # Client-side WASM components
|
||||
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||
├─ Services/ # HTTP API clients
|
||||
├─ Models/ # DTOs
|
||||
EnvelopeGenerator.Server/
|
||||
EnvelopeGenerator.Server/ # Server-side Blazor components
|
||||
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
||||
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||
├─ Services/ # HTTP API clients
|
||||
├─ Models/ # DTOs
|
||||
```
|
||||
|
||||
## Route Structure (Critical)
|
||||
|
||||
| Route | File Location | Render Mode | Purpose |
|
||||
|-------|--------------|-------------|---------|
|
||||
| `/` | `WebUI.Client/Pages/Index.razor` | WASM | Landing page |
|
||||
| `/sender/login` | `WebUI.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
||||
| `/sender` | `WebUI.Client/Pages/EnvelopeSenderPage.razor` | WASM | Sender dashboard |
|
||||
| `/envelope/login/{key}` | `WebUI.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
||||
| `/envelope/{key}` | `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
|
||||
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
|
||||
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
||||
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
|
||||
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
|
||||
| `/envelope/login/{key}` | `Server.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
||||
| `/envelope/{key}` | `Server/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
|
||||
|
||||
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
|
||||
|
||||
@@ -110,7 +171,7 @@ See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
||||
|
||||
## Status Color Coding
|
||||
|
||||
Form app uses DevExpress `CustomDrawCell`. WebUI needs CSS:
|
||||
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
|
||||
|
||||
```css
|
||||
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
||||
@@ -123,10 +184,10 @@ Form app uses DevExpress `CustomDrawCell`. WebUI needs CSS:
|
||||
|
||||
## Configuration
|
||||
|
||||
### YARP Proxy (`WebUI/yarp.json`)
|
||||
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
|
||||
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
||||
|
||||
### PDF.js Settings (`WebUI/wwwroot/appsettings.json`)
|
||||
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
|
||||
```json
|
||||
{
|
||||
"PdfViewerOptions": {
|
||||
@@ -177,7 +238,7 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
||||
|
||||
Manual testing workflow:
|
||||
1. Start API (`dotnet run` in `EnvelopeGenerator.API`)
|
||||
2. Start WebUI (`dotnet run` in `EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI`)
|
||||
2. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||
3. Navigate to `https://localhost:5131` (or check console output for port)
|
||||
4. Test sender login at `/sender/login`
|
||||
5. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||
@@ -245,7 +306,7 @@ Each envelope maintains independent authentication state.
|
||||
**CDN:**
|
||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||
|
||||
**NuGet (WebUI.Client):**
|
||||
**NuGet (Server.Client):**
|
||||
- `DevExpress.Blazor.*` 25.2.3
|
||||
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
||||
|
||||
|
||||
@@ -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,39 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
|
||||
/// </summary>
|
||||
public sealed record UserPreferencesDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The active DevExpress theme name.
|
||||
/// Allowed values: "blazing-berry", "fluent", "purple"
|
||||
/// </summary>
|
||||
public string ThemeName { get; set; } = "blazing-berry";
|
||||
|
||||
/// <summary>
|
||||
/// Whether dark mode is active.
|
||||
/// Default: false (light mode).
|
||||
/// </summary>
|
||||
public bool IsDarkMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
|
||||
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
|
||||
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
|
||||
/// </summary>
|
||||
public Dictionary<string, string> GridLayouts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The authenticated user's display name ("{GivenName} {Surname}").
|
||||
/// Populated by the server on GET responses from JWT claims.
|
||||
/// NOT persisted in cache — injected at read time by UserPreferencesController.
|
||||
/// Ignored on PUT requests.
|
||||
/// </summary>
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static UserPreferencesDto Default { get; } = new UserPreferencesDto();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
|
||||
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
|
||||
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||
/// </summary>
|
||||
public interface IUserPreferencesService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves preferences for the given user.
|
||||
/// Always returns a valid object — never throws, never returns null.
|
||||
/// Returns defaults if no entry exists in cache.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Persists the given preferences for the user in the distributed cache.
|
||||
/// Failures are non-fatal and silently ignored.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="preferences">The preferences to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default);
|
||||
}
|
||||
@@ -28,7 +28,9 @@ public class MappingProfile : Profile
|
||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>()
|
||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||
CreateMap<Document, DocumentDto>();
|
||||
CreateMap<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));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||
/// </summary>
|
||||
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
|
||||
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
|
||||
{
|
||||
}
|
||||
|
||||
@@ -53,7 +54,10 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
||||
/// </exception>
|
||||
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
||||
{
|
||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations);
|
||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
|
||||
|
||||
if (query.UserId is int userId)
|
||||
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||
|
||||
if (query.Id is not null)
|
||||
{
|
||||
|
||||
@@ -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,83 @@
|
||||
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;
|
||||
|
||||
envelope.Status = targetStatus;
|
||||
envelope.Comment = reason;
|
||||
envelope.ChangedWhen = DateTime.Now;
|
||||
|
||||
await _envelopeRepository.UpdateAsync(envelope, 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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
Imports System.IO
|
||||
Imports DigitalData.Modules.Database
|
||||
Imports DigitalData.Modules.Logging
|
||||
Imports EnvelopeGenerator.CommonServices
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||
Imports GdPicture14
|
||||
@@ -145,64 +146,91 @@ Public Class frmFinalizePDF
|
||||
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
||||
|
||||
' 3. Check what BurnAnnotsToPDF will do internally
|
||||
Dim oEnvelopeEntity As Envelope = Nothing
|
||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
||||
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
||||
oEnvelopeEntity = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
||||
|
||||
If envelope Is Nothing Then
|
||||
If oEnvelopeEntity Is Nothing Then
|
||||
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
||||
txtResult.Text = log.ToString()
|
||||
Return
|
||||
End If
|
||||
|
||||
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
|
||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
|
||||
log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}")
|
||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}")
|
||||
|
||||
If envelope.ReadOnly Then
|
||||
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
|
||||
txtResult.Text = log.ToString()
|
||||
Return
|
||||
End If
|
||||
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
.ToList()
|
||||
|
||||
log.AppendLine($"Elements (Signature) count: {elements.Count}")
|
||||
|
||||
If elements.Any() Then
|
||||
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
|
||||
For Each elem In elements
|
||||
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
|
||||
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
|
||||
If elem.Annotations IsNot Nothing Then
|
||||
For Each annot In elem.Annotations
|
||||
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
|
||||
Next
|
||||
End If
|
||||
Next
|
||||
If oEnvelopeEntity.ReadOnly Then
|
||||
log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
|
||||
Else
|
||||
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
.ToList()
|
||||
|
||||
log.AppendLine($"Elements (Signature) count: {elements.Count}")
|
||||
|
||||
If elements.Any() Then
|
||||
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
|
||||
For Each elem In elements
|
||||
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
|
||||
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
|
||||
If elem.Annotations IsNot Nothing Then
|
||||
For Each annot In elem.Annotations
|
||||
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
|
||||
Next
|
||||
End If
|
||||
Next
|
||||
Else
|
||||
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
||||
End If
|
||||
End If
|
||||
End Using
|
||||
|
||||
' 4. Actually call BurnAnnotsToPDF (same as Service)
|
||||
' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged)
|
||||
log.AppendLine("")
|
||||
log.AppendLine("Calling BurnAnnotsToPDF...")
|
||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||
log.AppendLine($"Result bytes: {oNewBuffer.Length}")
|
||||
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
|
||||
Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||
log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}")
|
||||
log.AppendLine($"Unchanged (ReadOnly): {oBuffer.Length = oBurnedBuffer.Length AndAlso oBuffer.SequenceEqual(oBurnedBuffer)}")
|
||||
|
||||
' 5. Write output
|
||||
' 5. Build State and load Envelope domain model for ReportCreator
|
||||
log.AppendLine("")
|
||||
log.AppendLine("Loading envelope domain model for report...")
|
||||
Dim oState As New State() With {
|
||||
.Database = Database,
|
||||
.LogConfig = LogConfig
|
||||
}
|
||||
Dim oEnvelopeModel As New EnvelopeModel(oState)
|
||||
Dim oEnvelope = oEnvelopeModel.GetById(envelopeId)
|
||||
If oEnvelope Is Nothing Then
|
||||
log.AppendLine("ERROR: Envelope not found via EnvelopeModel!")
|
||||
txtResult.Text = log.ToString()
|
||||
Return
|
||||
End If
|
||||
log.AppendLine($"Envelope loaded: {oEnvelope.Id}")
|
||||
|
||||
' 6. Create signing certificate report
|
||||
log.AppendLine("Creating signing certificate report...")
|
||||
Dim oReportCreator As New ReportCreator(LogConfig, oState)
|
||||
Dim oReportBuffer As Byte() = oReportCreator.CreateReport(oEnvelope)
|
||||
log.AppendLine($"Report bytes: {oReportBuffer.Length}")
|
||||
|
||||
' 7. Merge burned document + report (last page)
|
||||
log.AppendLine("Merging documents...")
|
||||
Dim oMerger As New PDFMerger(LogConfig, pGDPictureLicenseKey)
|
||||
Dim oMergedBuffer As Byte() = oMerger.MergeDocuments(oBurnedBuffer, oReportBuffer)
|
||||
log.AppendLine($"Merged bytes: {oMergedBuffer.Length}")
|
||||
|
||||
' 8. Save and open
|
||||
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
||||
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
|
||||
File.WriteAllBytes(oNewPath, oNewBuffer)
|
||||
log.AppendLine($"Output: {oNewPath}")
|
||||
Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf")
|
||||
File.WriteAllBytes(oMergedPath, oMergedBuffer)
|
||||
log.AppendLine($"Output: {oMergedPath}")
|
||||
|
||||
txtResult.Text = log.ToString()
|
||||
Process.Start(oNewPath)
|
||||
Process.Start(oMergedPath)
|
||||
|
||||
Catch ex As Exception
|
||||
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
||||
|
||||
@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
|
||||
Dim y = frameY + yOffsetofFF
|
||||
|
||||
If annot.Type = AnnotationType.FormField Then
|
||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
|
||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name)
|
||||
ElseIf annot.Type = AnnotationType.Image Then
|
||||
AddImageAnnotation(
|
||||
annot.X / inchFactor,
|
||||
@@ -269,11 +269,14 @@ Namespace Jobs.FinalizeDocument
|
||||
Next
|
||||
End Sub
|
||||
|
||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
|
||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String, Optional egName As String = "")
|
||||
Manager.SelectPage(page)
|
||||
|
||||
' Truncate if needed
|
||||
Dim displayValue As String = TruncateText(value, egName)
|
||||
|
||||
' Add the text annotation
|
||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
|
||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue)
|
||||
|
||||
' Set the font properties
|
||||
ant.FontName = _pdfBurnerParams.FontName
|
||||
@@ -294,8 +297,9 @@ Namespace Jobs.FinalizeDocument
|
||||
Dim oHeight = oBounds.Item(3)
|
||||
|
||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||
' Add the text annotation
|
||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
|
||||
' Add the text annotation (truncate if needed)
|
||||
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||
|
||||
' Set the font properties
|
||||
ant.FontName = _pdfBurnerParams.FontName
|
||||
@@ -306,6 +310,48 @@ Namespace Jobs.FinalizeDocument
|
||||
#End Region
|
||||
|
||||
#Region "Helpers"
|
||||
''' <summary>
|
||||
''' Truncates a field value so its rendered width fits within the configured max width (inches).
|
||||
''' Uses Graphics.MeasureString with the configured font for pixel-accurate measurement.
|
||||
''' Appends TruncationSuffix when the value is cut.
|
||||
''' </summary>
|
||||
Private Function TruncateText(value As String, fieldEgName As String) As String
|
||||
If String.IsNullOrEmpty(value) Then Return value
|
||||
If _pdfBurnerParams.TextMaxWidths Is Nothing Then Return value
|
||||
|
||||
Dim maxWidthInches As Single = 0
|
||||
If Not _pdfBurnerParams.TextMaxWidths.TryGetValue(fieldEgName, maxWidthInches) Then Return value
|
||||
If maxWidthInches <= 0 Then Return value
|
||||
|
||||
Dim suffix As String = If(_pdfBurnerParams.TruncationSuffix, "")
|
||||
|
||||
Using bmp As New Bitmap(1, 1)
|
||||
Using g As Graphics = Graphics.FromImage(bmp)
|
||||
Dim font As New Font(_pdfBurnerParams.FontName, _pdfBurnerParams.FontSize, _pdfBurnerParams.FontStyle, GraphicsUnit.Point)
|
||||
Dim maxWidthPx As Single = maxWidthInches * g.DpiX
|
||||
|
||||
' Already fits — no truncation needed
|
||||
If g.MeasureString(value, font).Width <= maxWidthPx Then Return value
|
||||
|
||||
' Binary search for the longest prefix that fits (with suffix)
|
||||
Dim lo As Integer = 0
|
||||
Dim hi As Integer = value.Length
|
||||
While lo < hi
|
||||
Dim mid As Integer = (lo + hi + 1) \ 2
|
||||
Dim candidate As String = value.Substring(0, mid) & suffix
|
||||
If g.MeasureString(candidate, font).Width <= maxWidthPx Then
|
||||
lo = mid
|
||||
Else
|
||||
hi = mid - 1
|
||||
End If
|
||||
End While
|
||||
|
||||
Return value.Substring(0, lo) & suffix
|
||||
End Using
|
||||
End Using
|
||||
End Function
|
||||
|
||||
|
||||
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||
|
||||
@@ -12,5 +12,24 @@ Namespace Jobs.FinalizeDocument
|
||||
Public Property FontSize As Integer = 8
|
||||
|
||||
Public Property FontStyle As FontStyle = FontStyle.Italic
|
||||
|
||||
''' <summary>
|
||||
''' The suffix appended to a truncated field value. Defaults to "..".
|
||||
''' Can be changed to any string (e.g. "…", "...", "").
|
||||
''' </summary>
|
||||
Public Property TruncationSuffix As String = ".."
|
||||
|
||||
''' <summary>
|
||||
''' Per-field maximum render width in inches.
|
||||
''' Text is measured using the configured Font/FontSize via Graphics.MeasureString (pixel-accurate).
|
||||
''' If the rendered width exceeds the limit, the text is cut and TruncationSuffix is appended.
|
||||
''' Keys must match egName values used in PDFBurner (e.g. "name", "position", "city", "date").
|
||||
''' Set a value to 0 to disable truncation for that field.
|
||||
''' </summary>
|
||||
Public Property TextMaxWidths As New Dictionary(Of String, Single) From {
|
||||
{"position", 1.41F},
|
||||
{"city", 1.41F},
|
||||
{"date", 2.0F}
|
||||
}
|
||||
End Class
|
||||
End Namespace
|
||||
@@ -1,5 +1,6 @@
|
||||
Imports EnvelopeGenerator.Domain.Constants
|
||||
Imports EnvelopeGenerator.Domain.Entities
|
||||
Imports EnvelopeGenerator.Domain.Interfaces
|
||||
|
||||
Public Class ReportItem
|
||||
|
||||
@@ -11,8 +12,15 @@ Public Class ReportItem
|
||||
Public Property ItemStatus As EnvelopeStatus
|
||||
Public ReadOnly Property ItemStatusTranslated As String
|
||||
Get
|
||||
Dim oStatus = ItemStatus.ToString()
|
||||
Return My.Resources.Model.ResourceManager.GetString(oStatus)
|
||||
Dim oStatusKey = ItemStatus.ToString()
|
||||
|
||||
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
|
||||
Envelope IsNot Nothing AndAlso
|
||||
Envelope.IsReadAndConfirm() Then
|
||||
oStatusKey = "DocumentSignedRaC"
|
||||
End If
|
||||
|
||||
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
|
||||
End Get
|
||||
End Property
|
||||
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
<data name="DocumentSigned" xml:space="preserve">
|
||||
<value>Document signed</value>
|
||||
</data>
|
||||
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||
<value>Read and confirmed</value>
|
||||
</data>
|
||||
<data name="Draft" xml:space="preserve">
|
||||
<value>Draft</value>
|
||||
</data>
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
<data name="DocumentSigned" xml:space="preserve">
|
||||
<value>Dokument unterzeichnet</value>
|
||||
</data>
|
||||
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||
<value>Gelesen und bestätigt</value>
|
||||
</data>
|
||||
<data name="Draft" xml:space="preserve">
|
||||
<value>Entwurf</value>
|
||||
</data>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Constants
|
||||
@@ -15,7 +16,11 @@ namespace EnvelopeGenerator.Domain.Constants
|
||||
public static class Receiver
|
||||
{
|
||||
public const string TFA = "EGReceiverTFA";
|
||||
|
||||
[Obsolete("Use Receiver.Authorized")]
|
||||
public const string Full = "EGReceiver";
|
||||
|
||||
public static readonly IReadOnlyList<string> Authorized = new[] { "receiver", "EGReceiver" };
|
||||
}
|
||||
|
||||
public const string Sender = "EGSender";
|
||||
|
||||
@@ -213,6 +213,15 @@ namespace My.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Gelesen und bestätigt.
|
||||
/// </summary>
|
||||
public static string DocumentSignedRaC {
|
||||
get {
|
||||
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Entwurf.
|
||||
/// </summary>
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
<data name="DocumentSigned" xml:space="preserve">
|
||||
<value>Dokument unterzeichnet</value>
|
||||
</data>
|
||||
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||
<value>Gelesen und bestätigt</value>
|
||||
</data>
|
||||
<data name="Draft" xml:space="preserve">
|
||||
<value>Entwurf</value>
|
||||
</data>
|
||||
|
||||
@@ -14,6 +14,7 @@ using EnvelopeGenerator.Infrastructure.Repositories;
|
||||
using EnvelopeGenerator.Infrastructure.Executor;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
#elif NETFRAMEWORK
|
||||
using System;
|
||||
@@ -55,6 +56,9 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
||||
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
||||
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
||||
|
||||
// User Preferences — distributed cache (dbo.TBDD_CACHE)
|
||||
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
|
||||
#endif
|
||||
|
||||
services.AddDbRepository(opt =>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
|
||||
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
#if NET
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Implements user preferences storage using the distributed cache (dbo.TBDD_CACHE).
|
||||
/// No DB schema changes required — uses the existing SQL Server distributed cache table.
|
||||
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||
/// Expiration: indefinite (no TTL) — preferences persist until explicitly overwritten.
|
||||
/// </summary>
|
||||
public sealed class UserPreferencesService(IDistributedCache cache) : IUserPreferencesService
|
||||
{
|
||||
private const string KeyPrefix = "signflow.user-preferences:";
|
||||
|
||||
private static string BuildKey(string id) => $"{KeyPrefix}{id}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
var json = await cache.GetStringAsync(BuildKey(id), ct);
|
||||
if(json is not null && JsonSerializer.Deserialize<UserPreferencesDto>(json) is UserPreferencesDto pref)
|
||||
{
|
||||
return pref;
|
||||
}
|
||||
else
|
||||
{
|
||||
// init new
|
||||
await SaveAsync(id, UserPreferencesDto.Default, ct);
|
||||
return UserPreferencesDto.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(preferences);
|
||||
|
||||
// No expiration — preferences are stored indefinitely.
|
||||
await cache.SetStringAsync(BuildKey(id), json, new DistributedCacheEntryOptions(), ct);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,12 +0,0 @@
|
||||
@using System.Globalization
|
||||
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -1,44 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class Adjustment
|
||||
{
|
||||
public static Adjustment CreateBalanceForward(DateTime dt, int random)
|
||||
{
|
||||
var rnd = new DeterministicRandom(random);
|
||||
Adjustment res = new Adjustment();
|
||||
res.currentDateTime = dt;
|
||||
res.currentDescription = "Balance Forward";
|
||||
res.currentAmount = rnd.Random(10, 300) * 10;
|
||||
return res;
|
||||
}
|
||||
public static Adjustment CreatePayment(DateTime dt, int random)
|
||||
{
|
||||
var rnd = new DeterministicRandom(random);
|
||||
Adjustment res = new Adjustment();
|
||||
res.currentDateTime = dt;
|
||||
res.currentDescription = "Payment";
|
||||
res.currentAmount = -rnd.Random(1, 40) * 10;
|
||||
return res;
|
||||
}
|
||||
public static Adjustment CreateCharge(DateTime dt, int random)
|
||||
{
|
||||
var rnd = new DeterministicRandom(random);
|
||||
Adjustment res = new Adjustment();
|
||||
res.currentDateTime = dt;
|
||||
res.currentDescription = rnd.GetRandomItem(bills);
|
||||
res.currentAmount = rnd.Random(10, 50) * 10;
|
||||
return res;
|
||||
}
|
||||
|
||||
DateTime currentDateTime;
|
||||
string currentDescription = "";
|
||||
double currentAmount = 0;
|
||||
static readonly string[] bills = new string[] { "Bill - Insurance", "Bill - Electricity", "Bill - Rent", "Bill - Phone", "Bill - Office Supplies" };
|
||||
public DateTime Date { get { return currentDateTime; } }
|
||||
public string Description { get { return currentDescription; } }
|
||||
public double Amount { get { return currentAmount; } }
|
||||
|
||||
public Adjustment()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using DevExpress.DataAccess.Sql;
|
||||
using DevExpress.DataAccess.Sql.DataApi;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class Customer {
|
||||
static List<Customer> currentCustomers = new List<Customer>();
|
||||
|
||||
public static List<Customer> Customers { get { return currentCustomers; } }
|
||||
static Customer() {
|
||||
try {
|
||||
SqlDataSource ds = new SqlDataSource("NWindConnectionString");
|
||||
SelectQuery query = SelectQueryFluentBuilder
|
||||
.AddTable("Customers")
|
||||
.SelectAllColumns()
|
||||
.Build("Customers");
|
||||
ds.Queries.Add(query);
|
||||
ds.RebuildResultSchema();
|
||||
ds.Fill();
|
||||
ITable src = ds.Result["Customers"];
|
||||
foreach(var row in src) {
|
||||
currentCustomers.Add(new Customer() {
|
||||
CustomerID = row.GetValue<string>("CustomerID"),
|
||||
Address = row.GetValue<string>("Address"),
|
||||
CompanyName = row.GetValue<string>("CompanyName"),
|
||||
ContactName = row.GetValue<string>("ContactName"),
|
||||
ContactTitle = row.GetValue<string>("ContactTitle"),
|
||||
Country = row.GetValue<string>("Country"),
|
||||
City = row.GetValue<string>("City"),
|
||||
Fax = row.GetValue<string>("Fax"),
|
||||
Phone = row.GetValue<string>("Phone"),
|
||||
PostalCode = row.GetValue<string>("PostalCode"),
|
||||
Region = row.GetValue<string>("Region")
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
currentCustomers.Add(new Customer() {
|
||||
Address = "Obere Str. 57",
|
||||
City = "Berlin",
|
||||
CompanyName = "Alfreds Futterkiste",
|
||||
ContactName = "Maria Anders",
|
||||
ContactTitle = "Sales Representative",
|
||||
Country = "Germany",
|
||||
CustomerID = "ALFKI",
|
||||
Fax = "030-0076545",
|
||||
Phone = "030-0074321",
|
||||
PostalCode = "12209"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomerID { get; set; }
|
||||
public string CompanyName { get; set; }
|
||||
public string ContactName { get; set; }
|
||||
public string ContactTitle { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string City { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public string Region { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string Phone { get; set; }
|
||||
public string Fax { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class DataItem {
|
||||
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
||||
public string CustomerID { get; set; }
|
||||
public string CompanyName { get; set; }
|
||||
public string ContactName { get; set; }
|
||||
public string ContactTitle { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string City { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public string Region { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string Phone { get; set; }
|
||||
public string Fax { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Invoice { get; set; }
|
||||
public string CustomerAccount { get; set; }
|
||||
public string CustomerIdentifiers { get; set; }
|
||||
public DateTime BillingDate { get; set; }
|
||||
public DateTime BillingPeriodStart { get; set; }
|
||||
public DateTime BillingPeriodEnd { get; set; }
|
||||
public string Terms { get; set; }
|
||||
public string TermsID { get; set; }
|
||||
public Adjustment[] Adjustments { get; set; }
|
||||
|
||||
public DataItem(int i) {
|
||||
var rnd = new DeterministicRandom(i);
|
||||
Customer c = rnd.GetRandomItem(Customer.Customers);
|
||||
CustomerID = c.CustomerID;
|
||||
CompanyName = c.CompanyName;
|
||||
ContactName = c.ContactName;
|
||||
ContactTitle = c.ContactTitle;
|
||||
Address = c.Address;
|
||||
City = c.City;
|
||||
PostalCode = c.PostalCode;
|
||||
Region = c.Region;
|
||||
Country = c.Country;
|
||||
Phone = c.Phone;
|
||||
Fax = c.Fax;
|
||||
Email = ContactName.Split(' ')[0].Replace(' ', '.').ToLower() + "@" + CompanyName.Split(' ')[0].ToLower() + ".com";
|
||||
Invoice = string.Format("{0}{1}-{2}", rnd.RandomChar, rnd.Random(100, 1000), rnd.Random(100, 1000));
|
||||
CustomerAccount = rnd.GetRandomItem(accountType);
|
||||
CustomerIdentifiers = string.Format("{0}-{1}", rnd.Random(1000, 10000), rnd.Random(10, 100));
|
||||
BillingPeriodStart = rnd.RandomTime();
|
||||
BillingPeriodEnd = rnd.RandomTime(BillingPeriodStart, 7 * 24, 30 * 24);
|
||||
BillingDate = rnd.RandomTime(BillingPeriodEnd, 7 * 24, 30 * 24);
|
||||
Term currentTerm = rnd.GetRandomItem(Term.Terms);
|
||||
Terms = currentTerm.Name;
|
||||
|
||||
int adjustmentsCount = rnd.Random(6) + 4;
|
||||
Adjustments = new Adjustment[adjustmentsCount];
|
||||
int h = (int)((BillingPeriodEnd - BillingPeriodStart).TotalHours / adjustmentsCount);
|
||||
|
||||
Adjustments[0] = Adjustment.CreateBalanceForward(rnd.RandomTime(BillingPeriodStart, 0, h), rnd.Random(10000));
|
||||
|
||||
int[] items = rnd.RandomList(adjustmentsCount - 1, 2);
|
||||
|
||||
for(int j = 1; j < Adjustments.Length; j++) {
|
||||
DateTime nextDate = rnd.RandomTime(BillingPeriodStart.AddHours(h * j), 0, h);
|
||||
switch(items[j - 1]) {
|
||||
case 0:
|
||||
Adjustments[j] = Adjustment.CreateCharge(nextDate, rnd.Random(10000));
|
||||
break;
|
||||
case 1:
|
||||
Adjustments[j] = Adjustment.CreatePayment(nextDate, rnd.Random(10000));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class DataItemList : IList<DataItem>, IList {
|
||||
readonly int rowCount;
|
||||
|
||||
public DataItem this[int index] { get { return new DataItem(index); } set { } }
|
||||
public int Count { get { return rowCount; } }
|
||||
public bool IsReadOnly { get { return false; } }
|
||||
public bool IsFixedSize { get { return false; } }
|
||||
public object SyncRoot { get { return true; } }
|
||||
public bool IsSynchronized { get { return true; } }
|
||||
object IList.this[int index] { get { return new DataItem(index); } set { } }
|
||||
|
||||
public DataItemList(int rowCount) {
|
||||
this.rowCount = rowCount;
|
||||
}
|
||||
public IEnumerator<DataItem> GetEnumerator() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public int Add(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public bool Contains(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Clear() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public int IndexOf(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Insert(int index, object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Remove(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void RemoveAt(int index) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void CopyTo(Array array, int index) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
IEnumerator IEnumerable.GetEnumerator() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public int IndexOf(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Insert(int index, DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Add(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public bool Contains(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void CopyTo(DataItem[] array, int arrayIndex) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public bool Remove(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
void ICollection<DataItem>.CopyTo(DataItem[] array, int arrayIndex) {
|
||||
CopyTo(array, arrayIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
class DeterministicRandom {
|
||||
const int randomCount = 10000;
|
||||
static readonly int[] deterministicRandomNumbers;
|
||||
static readonly DateTime time;
|
||||
int rnd;
|
||||
int Next {
|
||||
get {
|
||||
rnd = deterministicRandomNumbers[rnd % randomCount];
|
||||
return rnd;
|
||||
}
|
||||
}
|
||||
public char RandomChar {
|
||||
get {
|
||||
return (char)((int)'A' + Random(0, 26));
|
||||
}
|
||||
}
|
||||
public int[] RandomList(int count, int to) {
|
||||
int[] res = new int[count];
|
||||
for(int i = 0; i < Math.Min(count, to); i++)
|
||||
res[i] = i;
|
||||
for(int i = to; i < count; i++)
|
||||
res[i] = Random(to);
|
||||
|
||||
for(int i = 0; i < count; i++) {
|
||||
int ind = Random(count);
|
||||
int temp = res[ind];
|
||||
res[ind] = res[i];
|
||||
res[i] = temp;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
public int Random(int to) {
|
||||
return Random(0, to);
|
||||
}
|
||||
public int Random(int from, int to) {
|
||||
return Next % Math.Max(1, to - from) + from;
|
||||
}
|
||||
public T GetRandomItem<T>(IList<T> list) {
|
||||
return list[Next % list.Count];
|
||||
}
|
||||
public DateTime RandomTime() {
|
||||
return RandomTime(time, 0, 30 * 24);
|
||||
}
|
||||
public DateTime RandomTime(DateTime from, int fromHours, int toHours) {
|
||||
return from.AddHours(Next % (toHours - fromHours) + fromHours);
|
||||
}
|
||||
|
||||
static DeterministicRandom() {
|
||||
time = DateTime.Now.AddDays(-62);
|
||||
Random currentRandom = new Random(randomCount);
|
||||
deterministicRandomNumbers = new int[randomCount];
|
||||
for(int i = 0; i < randomCount; i++)
|
||||
deterministicRandomNumbers[i] = currentRandom.Next(randomCount);
|
||||
}
|
||||
public DeterministicRandom(int i) {
|
||||
this.rnd = i + (i >> 10) + (i >> 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public struct Term {
|
||||
public static readonly Term[] Terms = new Term[] {
|
||||
new Term("Payment seven days after invoice date" ),
|
||||
new Term("Payment ten days after invoice date" ),
|
||||
new Term("End of month" ),
|
||||
new Term("21st of the month following invoice date" ),
|
||||
};
|
||||
readonly string currentName;
|
||||
public string Name { get { return currentName; } }
|
||||
public Term(string currentName) {
|
||||
this.currentName = currentName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<WasmBuildNative>true</WasmBuildNative>
|
||||
<InvariantGlobalization>false</InvariantGlobalization>
|
||||
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>EnvelopeGenerator.ReceiverUI</PackageId>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>EnvelopeGenerator.ReceiverUI</Product>
|
||||
<PackageIcon>Assets\icon.ico</PackageIcon>
|
||||
<PackageTags>digital data envelope generator web</PackageTags>
|
||||
<Description>EnvelopeGenerator.ReceiverUI is a Blazor WebAssembly application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
||||
<Version>1.4.2</Version>
|
||||
<!-- NuGet package version -->
|
||||
<AssemblyVersion>1.4.2.0</AssemblyVersion>
|
||||
<!-- Assembly version for API compatibility -->
|
||||
<FileVersion>1.4.2.0</FileVersion>
|
||||
<!-- Windows file version -->
|
||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.8" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.8" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.8" />
|
||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.8" />
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.28" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.11" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\PublishProfiles\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="wwwroot\docs\privacy-policy.fr-FR.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants
|
||||
{
|
||||
public enum SenderAppType
|
||||
{
|
||||
LegacyFormApp = 0,
|
||||
ReceiverUIBlazorApp = 1
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the unit of measurement for coordinate values in signature positioning.
|
||||
/// Used for converting coordinates between different systems (GdPicture14, PDF.js, iText7).
|
||||
/// </summary>
|
||||
public enum UnitOfLength
|
||||
{
|
||||
/// <summary>
|
||||
/// Inch unit (1 inch = 25.4 mm).
|
||||
/// This is the native unit used by GdPicture14 (EnvelopeGenerator.Form - Legacy VB.NET app).
|
||||
/// Database stores all coordinates (X, Y, Width, Height) in INCHES.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Source:</b> GdPicture14.Annotations.AnnotationStickyNote uses INCHES natively.
|
||||
/// <br/>
|
||||
/// <b>Evidence:</b> VB.NET code directly assigns database values to annotation properties without conversion:
|
||||
/// <code>
|
||||
/// oAnnotation.Left = CSng(pElement.X) ' Direct assignment → INCHES
|
||||
/// oAnnotation.Top = CSng(pElement.Y)
|
||||
/// </code>
|
||||
/// <b>Standard Page Dimensions:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>A4: 8.27" × 11.69" (210mm × 297mm)</item>
|
||||
/// <item>Letter: 8.5" × 11"</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Inch = 0,
|
||||
|
||||
/// <summary>
|
||||
/// PDF Point unit (1 point = 1/72 inch).
|
||||
/// This is the standard unit used by PDF specification and PDF.js viewer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Definition:</b> According to PDF specification and Microsoft documentation:
|
||||
/// <br/>
|
||||
/// <i>"PDF pages are sized in point units. 1 pt == 1/72 inch"</i>
|
||||
/// <br/><br/>
|
||||
/// <b>Conversion Formula:</b>
|
||||
/// <code>
|
||||
/// points = inches * 72.0
|
||||
/// inches = points / 72.0
|
||||
/// </code>
|
||||
/// <b>Important:</b> Point ≠ Pixel!
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Point (pt):</b> Device-independent unit (always 1/72 inch)</item>
|
||||
/// <item><b>Pixel (px):</b> Device-dependent unit (varies with screen DPI)</item>
|
||||
/// <item>At 72 DPI: 1 point = 1 pixel (coincidence)</item>
|
||||
/// <item>At 96 DPI: 1 point ≈ 1.33 pixels</item>
|
||||
/// <item>At 300 DPI: 1 point ≈ 4.17 pixels</item>
|
||||
/// </list>
|
||||
/// <b>Standard Page Dimensions (in points):</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>A4: 595 × 842 points (8.27" × 11.69" × 72)</item>
|
||||
/// <item>Letter: 612 × 792 points (8.5" × 11" × 72)</item>
|
||||
/// </list>
|
||||
/// <b>Usage in EnvelopeGenerator:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>PDF.js viewer expects coordinates in points</item>
|
||||
/// <item>iText7 library uses points for PDF manipulation</item>
|
||||
/// <item>PSPDFKit (Web) uses points for annotation placement</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Point
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
|
||||
public class EnvelopeDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("uuid")]
|
||||
public string? Uuid { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonPropertyName("docResult")]
|
||||
public byte[]? DocResult { get; set; }
|
||||
|
||||
[JsonPropertyName("envelopeReceivers")]
|
||||
public List<EnvelopeReceiverSimpleDto> EnvelopeReceivers { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simplified receiver model for envelope list display
|
||||
/// </summary>
|
||||
public class EnvelopeReceiverSimpleDto
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[JsonPropertyName("signed")]
|
||||
public bool Signed { get; set; }
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side model for the envelope receiver returned by
|
||||
/// <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||
/// </summary>
|
||||
public record EnvelopeReceiverDto
|
||||
{
|
||||
public int EnvelopeId { get; init; }
|
||||
public int ReceiverId { get; init; }
|
||||
public int Sequence { get; init; }
|
||||
|
||||
public string? Name { get; init; }
|
||||
public string? JobTitle { get; init; }
|
||||
public string? CompanyName { get; init; }
|
||||
public string? PrivateMessage { get; init; }
|
||||
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public DateTime? ChangedWhen { get; init; }
|
||||
public bool HasPhoneNumber { get; init; }
|
||||
|
||||
public EnvelopeClientDto? Envelope { get; init; }
|
||||
public ReceiverClientDto? Receiver { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side model for the envelope data embedded in <see cref="EnvelopeReceiverDto"/>.
|
||||
/// </summary>
|
||||
public record EnvelopeClientDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public int UserId { get; init; }
|
||||
public int Status { get; init; }
|
||||
public string StatusName { get; init; } = string.Empty;
|
||||
public string Uuid { get; init; } = string.Empty;
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public DateTime? ChangedWhen { get; init; }
|
||||
public string Language { get; init; } = "de-DE";
|
||||
public int? EnvelopeTypeId { get; init; }
|
||||
public string? EnvelopeTypeTitle { get; init; }
|
||||
public int? ContractType { get; init; }
|
||||
public int? CertificationType { get; init; }
|
||||
public bool UseAccessCode { get; init; }
|
||||
public bool TFAEnabled { get; init; }
|
||||
public IEnumerable<DocumentClientDto>? Documents { get; init; }
|
||||
public EnvelopeSenderDto? User { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sender (user) information embedded in <see cref="EnvelopeClientDto"/>.
|
||||
/// </summary>
|
||||
public record EnvelopeSenderDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string? Username { get; init; }
|
||||
public string? FullName { get; init; }
|
||||
public string? Email { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side model for a document embedded in <see cref="EnvelopeClientDto"/>.
|
||||
/// </summary>
|
||||
public record DocumentClientDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public int EnvelopeId { get; init; }
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public IEnumerable<SignatureClientDto>? Elements { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side model for a signature/annotation element embedded in <see cref="DocumentClientDto"/>.
|
||||
/// </summary>
|
||||
public record SignatureClientDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public int DocumentId { get; init; }
|
||||
public int ReceiverId { get; init; }
|
||||
public int ElementType { get; init; }
|
||||
public double X { get; init; }
|
||||
public double Y { get; init; }
|
||||
public double Width { get; init; }
|
||||
public double Height { get; init; }
|
||||
public int Page { get; init; }
|
||||
public bool Required { get; init; }
|
||||
public string? Tooltip { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public int AnnotationIndex { get; init; }
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public DateTime? ChangedWhen { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side model for the receiver data embedded in <see cref="EnvelopeReceiverDto"/>.
|
||||
/// </summary>
|
||||
public record ReceiverClientDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string? EmailAddress { get; init; }
|
||||
public string? Signature { get; init; }
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public DateTime? TfaRegDeadline { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
||||
/// This model holds the signature image (as base64 data URL) along with signer information
|
||||
/// used for rendering applied signatures on the PDF canvas.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
|
||||
/// <br/>
|
||||
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
|
||||
/// <br/>
|
||||
/// <b>Storage:</b> Session-only (Blazor component state, lost on page refresh)
|
||||
/// <br/>
|
||||
/// <b>Rendering:</b> Applied signatures display: Image + Separator + Name/Position/Place/Date
|
||||
/// </remarks>
|
||||
public sealed record SignatureCaptureDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded data URL of the signature image.
|
||||
/// <br/>
|
||||
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
|
||||
/// <br/>
|
||||
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
|
||||
/// <br/>
|
||||
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
|
||||
/// </summary>
|
||||
public required string DataUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Full name of the signer (first and last name).
|
||||
/// <br/>
|
||||
/// <b>Required:</b> Yes (validated in popup)
|
||||
/// <br/>
|
||||
/// <b>Display:</b> Bold text in applied signature block
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Max Mustermann"
|
||||
/// </summary>
|
||||
public required string FullName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Job title or position of the signer.
|
||||
/// <br/>
|
||||
/// <b>Required:</b> No (optional field)
|
||||
/// <br/>
|
||||
/// <b>Display:</b> Normal weight text between name and place/date
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Geschäftsführer" or empty string
|
||||
/// </summary>
|
||||
public string Position { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Location/place where the signature was created.
|
||||
/// <br/>
|
||||
/// <b>Required:</b> Yes (validated in popup)
|
||||
/// <br/>
|
||||
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||
/// </summary>
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Models.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a signature position on a PDF page.
|
||||
/// Coordinates stored in INCHES (GdPicture14 native unit).
|
||||
/// Origin: Top-left corner, X increases right, Y increases down.
|
||||
/// </summary>
|
||||
public class SignatureDto
|
||||
{
|
||||
/// <summary>Unique identifier.</summary>
|
||||
public int Id { get; init; }
|
||||
|
||||
private double _x;
|
||||
private double _y;
|
||||
|
||||
/// <summary>Horizontal position in INCHES from left edge.</summary>
|
||||
public double X
|
||||
{
|
||||
get => _x * Factor;
|
||||
init => _x = value;
|
||||
}
|
||||
|
||||
/// <summary>Vertical position in INCHES from top edge.</summary>
|
||||
public double Y
|
||||
{
|
||||
get => _y * Factor;
|
||||
init => _y = value;
|
||||
}
|
||||
|
||||
/// <summary>1-based page number.</summary>
|
||||
public int Page { get; init; }
|
||||
|
||||
/// <summary>Sender application type that created this signature.</summary>
|
||||
public SenderAppType SenderAppType { get; init; }
|
||||
|
||||
private UnitOfLength _unitOfLength;
|
||||
|
||||
public SignatureDto Convert(UnitOfLength unitOfLength)
|
||||
{
|
||||
_unitOfLength = unitOfLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
public double Factor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SenderAppType != SenderAppType.LegacyFormApp)
|
||||
{
|
||||
throw new NotImplementedException(
|
||||
$"SenderAppType '{SenderAppType}' is not yet implemented. " +
|
||||
$"Currently, only '{nameof(SenderAppType.LegacyFormApp)}' is supported. " +
|
||||
$"Future implementations will handle '{nameof(SenderAppType.ReceiverUIBlazorApp)}' and other types.");
|
||||
}
|
||||
|
||||
// LegacyFormApp uses GdPicture14 with INCHES
|
||||
return _unitOfLength switch
|
||||
{
|
||||
UnitOfLength.Inch => 1.0, // No conversion needed: INCHES → INCHES
|
||||
UnitOfLength.Point => 72.0, // INCHES → PDF Points: 1 inch = 72 points (PDF standard, NOT pixels!)
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unknown UnitOfLength: {_unitOfLength}. Expected '{nameof(UnitOfLength.Inch)}' or '{nameof(UnitOfLength.Point)}'.")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SignatureDtoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts all signatures in the collection to the specified unit of length.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the collection (IEnumerable, List, etc.)</typeparam>
|
||||
/// <param name="signatures">Collection of SignatureDto objects to convert.</param>
|
||||
/// <param name="unitOfLength">Target unit of measurement (Inch or Point).</param>
|
||||
/// <returns>The same collection with all signatures converted to the specified unit.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when signatures collection is null.</exception>
|
||||
/// <remarks>
|
||||
/// <b>Usage:</b>
|
||||
/// <code>
|
||||
/// var signatures = await SignatureService.GetAsync(envelopeKey);
|
||||
/// var convertedSignatures = signatures.ConvertAll(UnitOfLength.Point);
|
||||
/// </code>
|
||||
/// <b>Note:</b> This method modifies each SignatureDto object in place and returns the same collection.
|
||||
/// </remarks>
|
||||
public static T Convert<T>(this T signatures, UnitOfLength unitOfLength)
|
||||
where T : IEnumerable<SignatureDto>
|
||||
{
|
||||
if (signatures == null)
|
||||
throw new ArgumentNullException(nameof(signatures));
|
||||
|
||||
foreach (var signature in signatures)
|
||||
{
|
||||
signature.Convert(unitOfLength);
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
The following open source libraries are used and included within thеse sample/demonstration projects:
|
||||
|
||||
Bootstrap (Open Source – MIT License)
|
||||
Copyright (c) 2011-2021 Twitter, Inc. / Copyright (c) 2011-2021 The Bootstrap Authors
|
||||
https://github.com/twbs/bootstrap/blob/main/LICENSE
|
||||
|
||||
open-iconic (Open Source – MIT License and SIL Open Font License)
|
||||
Copyright (c) 2014 Waybury
|
||||
https://github.com/iconic/open-iconic/blob/master/ICON-LICENSE
|
||||
https://github.com/iconic/open-iconic/blob/master/FONT-LICENSE
|
||||
|
||||
The open source libraries included in these sample/demonstration projects are done so pursuant to each individual open source library license and subject to the disclaimers and limitations on liability set forth in each open source library license.
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Options;
|
||||
|
||||
public class ApiOptions
|
||||
{
|
||||
public const string SectionName = "Api";
|
||||
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
public bool UsePredefinedReports { get; set; } = false;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Options;
|
||||
|
||||
public class PdfViewerOptions
|
||||
{
|
||||
public const string SectionName = "PdfViewer";
|
||||
|
||||
/// <summary>
|
||||
/// Base scale for thumbnail rendering (0.2 - 1.5 recommended)
|
||||
/// Higher values = better quality but slower rendering
|
||||
/// Default: 0.75
|
||||
/// </summary>
|
||||
public double ThumbnailBaseScale { get; set; } = 0.75;
|
||||
|
||||
/// <summary>
|
||||
/// Enable HiDPI/Retina support for thumbnails
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0)
|
||||
/// Caps DPR to avoid excessive memory usage on 4K+ displays
|
||||
/// Default: 2.0
|
||||
/// </summary>
|
||||
public double ThumbnailMaxDPR { get; set; } = 2.0;
|
||||
|
||||
/// <summary>
|
||||
/// Enable HiDPI/Retina support for main PDF canvas
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0)
|
||||
/// Default: 2.0
|
||||
/// </summary>
|
||||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
||||
|
||||
/// <summary>
|
||||
/// Enable smooth zoom transition (fade effect)
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool EnableSmoothZoom { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Zoom transition duration in milliseconds (50 - 500)
|
||||
/// Default: 150
|
||||
/// </summary>
|
||||
public int ZoomTransitionDuration { get; set; } = 150;
|
||||
|
||||
/// <summary>
|
||||
/// Opacity during rendering (0.0 - 1.0)
|
||||
/// Lower values = more visible fade effect
|
||||
/// Default: 0.85
|
||||
/// </summary>
|
||||
public double RenderingOpacity { get; set; } = 0.85;
|
||||
|
||||
/// <summary>
|
||||
/// Delay between thumbnail renders in milliseconds (10 - 200)
|
||||
/// Higher values = less browser stress, slower initial load
|
||||
/// Default: 50
|
||||
/// </summary>
|
||||
public int ThumbnailRenderDelay { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Zoom step percentage (1 - 50)
|
||||
/// Controls how much zoom changes per click or scroll
|
||||
/// Default: 5 (5% per step)
|
||||
/// </summary>
|
||||
public int ZoomStepPercentage { get; set; } = 5;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,112 +0,0 @@
|
||||
@page "/envelope/DxPdfViewer"
|
||||
@using System.IO
|
||||
@using DevExpress.Blazor
|
||||
@using System.Reflection
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
.custom-drop-zone {
|
||||
padding: 0 !important;
|
||||
border-style: dashed;
|
||||
border-width: 2px !important;
|
||||
height: 230px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(183, 183, 183, 0.1);
|
||||
}
|
||||
|
||||
.custom-drop-zone.custom-drop-zone-hover {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.custom-drop-zone svg {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.custom-drop-zone > *:not(#overviewDemoSelectButton) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pdf-viewer {
|
||||
height: 800px !important;
|
||||
min-height: 800px !important;
|
||||
}
|
||||
|
||||
.pdf-viewer .dxbrv-surface-wrapper,
|
||||
.pdf-viewer .dxbrv-document-surface {
|
||||
height: 100% !important;
|
||||
min-height: 750px !important;
|
||||
}
|
||||
|
||||
.pdf-viewer .dxbrv-report-preview-content {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
min-width: 200px !important;
|
||||
min-height: 200px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="overviewDemoDropZone" class="card custom-drop-zone rounded-3 w-100 m-0">
|
||||
<span class="drop-file-icon mb-3"></span>
|
||||
<span class="drop-file-label">Drag and Drop File Here</span><span class="m-1">or</span>
|
||||
<DxButton Id="overviewDemoSelectButton"
|
||||
CssClass="m-1"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Text="Select File" />
|
||||
</div>
|
||||
<DxFileInput @ref="fileInput"
|
||||
AcceptedFileTypes="@ALLOWED_FILE_TYPES"
|
||||
AllowedFileExtensions="@ALLOWED_FILE_TYPES"
|
||||
CssClass="w-100"
|
||||
ExternalDropZoneCssSelector="#overviewDemoDropZone"
|
||||
ExternalDropZoneDragOverCssClass="custom-drop-zone-hover"
|
||||
ExternalSelectButtonCssSelector="#overviewDemoSelectButton"
|
||||
FilesUploading="OnFilesUploading"
|
||||
MaxFileSize="2000000">
|
||||
</DxFileInput>
|
||||
|
||||
@if (DocumentContent != null && DocumentContent.Length > 0)
|
||||
{
|
||||
<div class="alert alert-success mt-3">
|
||||
PDF loaded: @DocumentContent.Length bytes
|
||||
</div>
|
||||
<DxPdfViewer CssClass="w-100 pdf-viewer" DocumentContent="@DocumentContent" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info mt-3">
|
||||
Please upload a PDF file to view it.
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
readonly List<string> ALLOWED_FILE_TYPES = new List<string> { ".pdf" };
|
||||
DxFileInput fileInput;
|
||||
byte[] DocumentContent { get; set; }
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.ReceiverUI.Resources.Invoice.pdf");
|
||||
if (stream != null)
|
||||
{
|
||||
using (stream)
|
||||
using (var binaryReader = new BinaryReader(stream))
|
||||
DocumentContent = binaryReader.ReadBytes((int)stream.Length);
|
||||
}
|
||||
}
|
||||
protected async Task OnFilesUploading(FilesUploadingEventArgs args)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
{
|
||||
IFileInputSelectedFile file = args.Files[0];
|
||||
await file.OpenReadStream(file.Size).CopyToAsync(stream);
|
||||
DocumentContent = stream.ToArray();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
@page "/envelope/{EnvelopeKey}/DxReportViewer"
|
||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@using Microsoft.Extensions.Options
|
||||
@using EnvelopeGenerator.ReceiverUI.Options
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||
@inject DocumentService DocumentService
|
||||
@inject IOptions<ApiOptions> AppOptions
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||
|
||||
|
||||
@if (_report is not null) {
|
||||
<DxReportViewer Report="_report" RootCssClasses="w-100 h-100" Zoom="1.3" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string EnvelopeKey { get; init; } = null!;
|
||||
|
||||
XtraReport? _report = null;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_report = await CreateReport();
|
||||
}
|
||||
|
||||
async Task<XtraReport> CreateReport()
|
||||
{
|
||||
if (AppOptions.Value.UsePredefinedReports)
|
||||
{
|
||||
return PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
|
||||
if (pdfBytes is null || pdfBytes.Length == 0)
|
||||
throw new InvalidOperationException($"No PDF bytes found for EnvelopeKey: {EnvelopeKey}");
|
||||
|
||||
var report = new XtraReport();
|
||||
var detail = new DevExpress.XtraReports.UI.DetailBand();
|
||||
report.Bands.Add(detail);
|
||||
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = pdfBytes, GenerateOwnPages = true });
|
||||
return report;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
@page "/envelope/Embed"
|
||||
@using System.IO
|
||||
@using DevExpress.Blazor
|
||||
@using System.Reflection
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
.custom-drop-zone {
|
||||
padding: 0 !important;
|
||||
border-style: dashed;
|
||||
border-width: 2px !important;
|
||||
height: 230px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(183, 183, 183, 0.1);
|
||||
}
|
||||
|
||||
.custom-drop-zone.custom-drop-zone-hover {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.custom-drop-zone svg {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.custom-drop-zone > *:not(#overviewDemoSelectButton) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pdf-viewer {
|
||||
height: 800px !important;
|
||||
min-height: 800px !important;
|
||||
}
|
||||
|
||||
.pdf-viewer .dxbrv-surface-wrapper,
|
||||
.pdf-viewer .dxbrv-document-surface {
|
||||
height: 100% !important;
|
||||
min-height: 750px !important;
|
||||
}
|
||||
|
||||
.pdf-viewer .dxbrv-report-preview-content {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
min-width: 200px !important;
|
||||
min-height: 200px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="overviewDemoDropZone" class="card custom-drop-zone rounded-3 w-100 m-0">
|
||||
<span class="drop-file-icon mb-3"></span>
|
||||
<span class="drop-file-label">Drag and Drop File Here</span><span class="m-1">or</span>
|
||||
<DxButton Id="overviewDemoSelectButton"
|
||||
CssClass="m-1"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Text="Select File" />
|
||||
</div>
|
||||
<DxFileInput @ref="fileInput"
|
||||
AcceptedFileTypes="@ALLOWED_FILE_TYPES"
|
||||
AllowedFileExtensions="@ALLOWED_FILE_TYPES"
|
||||
CssClass="w-100"
|
||||
ExternalDropZoneCssSelector="#overviewDemoDropZone"
|
||||
ExternalDropZoneDragOverCssClass="custom-drop-zone-hover"
|
||||
ExternalSelectButtonCssSelector="#overviewDemoSelectButton"
|
||||
FilesUploading="OnFilesUploading"
|
||||
MaxFileSize="2000000">
|
||||
</DxFileInput>
|
||||
|
||||
@if (DocumentContent != null && DocumentContent.Length > 0)
|
||||
{
|
||||
<div class="alert alert-success mt-3">
|
||||
PDF loaded: @DocumentContent.Length bytes
|
||||
</div>
|
||||
<embed src="@GetPdfDataUrl()" type="application/pdf" class="w-100 pdf-viewer" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info mt-3">
|
||||
Please upload a PDF file to view it.
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
readonly List<string> ALLOWED_FILE_TYPES = new List<string> { ".pdf" };
|
||||
DxFileInput fileInput;
|
||||
byte[] DocumentContent { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.ReceiverUI.Resources.Invoice.pdf");
|
||||
if (stream != null)
|
||||
{
|
||||
using (stream)
|
||||
using (var binaryReader = new BinaryReader(stream))
|
||||
DocumentContent = binaryReader.ReadBytes((int)stream.Length);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task OnFilesUploading(FilesUploadingEventArgs args)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
{
|
||||
IFileInputSelectedFile file = args.Files[0];
|
||||
await file.OpenReadStream(file.Size).CopyToAsync(stream);
|
||||
DocumentContent = stream.ToArray();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetPdfDataUrl()
|
||||
{
|
||||
if (DocumentContent == null || DocumentContent.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
string base64 = Convert.ToBase64String(DocumentContent);
|
||||
return $"data:application/pdf;base64,{base64}#toolbar=0&navpanes=0&scrollbar=1";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
@page "/sender"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "Sender")]
|
||||
|
||||
@using System.Text.Json
|
||||
@using EnvelopeGenerator.Domain.Constants
|
||||
@using EnvelopeGenerator.ReceiverUI.Models
|
||||
@using DevExpress.Blazor
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeService EnvelopeService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AppVersionService AppVersion
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||
<link href="@AppVersion.GetVersionedUrl("css/sender-page.css")" rel="stylesheet" />
|
||||
|
||||
<div class="sender-dashboard-layout">
|
||||
<div class="sender-action-bar">
|
||||
<div class="sender-action-bar__inner">
|
||||
<div class="sender-title-section">
|
||||
<div class="sender-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" 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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="sender-title">Umschlag-Übersicht</div>
|
||||
</div>
|
||||
|
||||
<div class="sender-toolbar">
|
||||
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
</svg>
|
||||
Neuer Umschlag
|
||||
</button>
|
||||
|
||||
<button class="sender-btn" @onclick="EditEnvelope" disabled="@(_selectedEnvelope == null || IsEnvelopeSent(_selectedEnvelope))" title="Ausgewählten Umschlag bearbeiten">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
Bearbeiten
|
||||
</button>
|
||||
|
||||
<button class="sender-btn sender-btn--danger" @onclick="DeleteEnvelope" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag löschen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
|
||||
<button class="sender-btn" @onclick="RefreshEnvelopes" disabled="@_isLoading" title="Aktualisieren">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||
</svg>
|
||||
@if (_isLoading) {
|
||||
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
||||
}
|
||||
</button>
|
||||
|
||||
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden">
|
||||
@if (_isLoggingOut) {
|
||||
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sender-content">
|
||||
@if (_isLoading && _allEnvelopes == null) {
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
||||
<span class="visually-hidden">Lädt...</span>
|
||||
</div>
|
||||
<p class="text-white fw-semibold">Umschläge werden geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
} else if (_errorMessage != null) {
|
||||
<div class="error-container">
|
||||
<div class="alert alert-danger shadow-lg">
|
||||
<div class="d-flex align-items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<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>
|
||||
<div>
|
||||
<h5 class="mb-2">Fehler beim Laden der Umschläge</h5>
|
||||
<p class="mb-0">@_errorMessage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<div class="sender-grid-container">
|
||||
<div class="sender-tabs">
|
||||
<button class="sender-tab @(_activeTab == "active" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "active"'>
|
||||
<span>Aktive Umschläge</span>
|
||||
@if (_activeEnvelopes != null) {
|
||||
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_activeEnvelopes.Count())</span>
|
||||
}
|
||||
</button>
|
||||
<button class="sender-tab @(_activeTab == "completed" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "completed"'>
|
||||
<span>Abgeschlossene Umschläge</span>
|
||||
@if (_completedEnvelopes != null) {
|
||||
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_completedEnvelopes.Count())</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sender-grid-wrapper">
|
||||
@if (_activeTab == "active") {
|
||||
<DxGrid Data="@_activeEnvelopes"
|
||||
@ref="_gridActive"
|
||||
ShowFilterRow="true"
|
||||
ShowSearchBox="true"
|
||||
AllowColumnReorder="true"
|
||||
AllowSort=true
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
PageSize="20"
|
||||
PagerVisible="true"
|
||||
SelectionMode="GridSelectionMode.Single"
|
||||
SelectedDataItem="@_selectedEnvelope"
|
||||
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
||||
CustomizeElement="OnCustomizeElement">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" Caption="ID">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Title" Caption="Titel">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="Status">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
var statusInfo = GetStatusInfo(envelope.Status);
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
|
||||
var signed = receivers.Count(r => r.Signed);
|
||||
var total = receivers.Count;
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
||||
@signed / @total unterschrieben
|
||||
</span>
|
||||
@if (total > 0) {
|
||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
</Columns>
|
||||
<DetailRowTemplate Context="detailContext">
|
||||
<div style="padding: 1rem; background: #f9fafb;">
|
||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
||||
@{
|
||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true) {
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers) {
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
||||
@if (receiver.Signed) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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"/>
|
||||
</svg>
|
||||
<span>Unterschrieben</span>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
<span>Ausstehend</span>
|
||||
}
|
||||
</span>
|
||||
<div style="flex: 1; font-size: 0.875rem;">
|
||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Email</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</DetailRowTemplate>
|
||||
</DxGrid>
|
||||
} else {
|
||||
<DxGrid Data="@_completedEnvelopes"
|
||||
@ref="_gridCompleted"
|
||||
ShowFilterRow="true"
|
||||
ShowSearchBox="true"
|
||||
PageSize="20"
|
||||
PagerVisible="true"
|
||||
SelectionMode="GridSelectionMode.Single"
|
||||
SelectedDataItem="@_selectedEnvelope"
|
||||
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
||||
CustomizeElement="OnCustomizeElement">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" Caption="ID">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Title" Caption="Titel">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="Status">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
var statusInfo = GetStatusInfo(envelope.Status);
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
|
||||
var signed = receivers.Count(r => r.Signed);
|
||||
var total = receivers.Count;
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
||||
@signed / @total unterschrieben
|
||||
</span>
|
||||
@if (total > 0) {
|
||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
</Columns>
|
||||
<DetailRowTemplate Context="detailContext">
|
||||
<div style="padding: 1rem; background: #f9fafb;">
|
||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
||||
@{
|
||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true) {
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers) {
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
||||
@if (receiver.Signed) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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"/>
|
||||
</svg>
|
||||
<span>Unterschrieben</span>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
<span>Ausstehend</span>
|
||||
}
|
||||
</span>
|
||||
<div style="flex: 1; font-size: 0.875rem;">
|
||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Email</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</DetailRowTemplate>
|
||||
</DxGrid>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private IEnumerable<EnvelopeDto>? _allEnvelopes;
|
||||
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
||||
private IEnumerable<EnvelopeDto>? _completedEnvelopes;
|
||||
private EnvelopeDto? _selectedEnvelope;
|
||||
private string _activeTab = "active";
|
||||
private bool _isLoading = true;
|
||||
private bool _isLoggingOut = false;
|
||||
private string? _errorMessage;
|
||||
private DxGrid? _gridActive;
|
||||
private DxGrid? _gridCompleted;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var hasAccess = await AuthService.CheckSenderAsync();
|
||||
if (!hasAccess)
|
||||
{
|
||||
Navigation.NavigateTo($"/sender/login");
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadEnvelopesAsync();
|
||||
}
|
||||
|
||||
async Task LoadEnvelopesAsync()
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
||||
|
||||
// Split into active and completed based on status
|
||||
var envelopes = _allEnvelopes.ToList();
|
||||
_activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList();
|
||||
_completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("console.log", $"Loaded {_activeEnvelopes.Count()} active and {_completedEnvelopes.Count()} completed envelopes");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = ex.Message;
|
||||
await JSRuntime.InvokeVoidAsync("console.error", "Fehler beim Laden der Umschläge:", ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
async Task RefreshEnvelopes()
|
||||
{
|
||||
await LoadEnvelopesAsync();
|
||||
}
|
||||
|
||||
void CreateEnvelope()
|
||||
{
|
||||
// TODO: Navigate to envelope creation page
|
||||
JSRuntime.InvokeVoidAsync("console.log", "Create envelope clicked - not yet implemented");
|
||||
}
|
||||
|
||||
void EditEnvelope()
|
||||
{
|
||||
if (_selectedEnvelope == null) return;
|
||||
// TODO: Navigate to envelope editor
|
||||
JSRuntime.InvokeVoidAsync("console.log", $"Edit envelope {_selectedEnvelope.Id} clicked - not yet implemented");
|
||||
}
|
||||
|
||||
void DeleteEnvelope()
|
||||
{
|
||||
if (_selectedEnvelope == null) return;
|
||||
// TODO: Show delete confirmation dialog
|
||||
JSRuntime.InvokeVoidAsync("console.log", $"Delete envelope {_selectedEnvelope.Id} clicked - not yet implemented");
|
||||
}
|
||||
|
||||
async Task LogoutAsync()
|
||||
{
|
||||
_isLoggingOut = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await AuthService.LogoutSenderAsync();
|
||||
Navigation.NavigateTo("/sender/login", forceLoad: true);
|
||||
}
|
||||
|
||||
bool IsEnvelopeSent(EnvelopeDto envelope)
|
||||
{
|
||||
var status = (EnvelopeStatus)envelope.Status;
|
||||
return status >= EnvelopeStatus.EnvelopeQueued;
|
||||
}
|
||||
|
||||
(string Label, string CssClass, string DotColor) GetStatusInfo(int statusCode)
|
||||
{
|
||||
var status = (EnvelopeStatus)statusCode;
|
||||
return status switch
|
||||
{
|
||||
EnvelopeStatus.EnvelopePartlySigned => ("Teilweise unterschrieben", "partly-signed", "green"),
|
||||
EnvelopeStatus.EnvelopeQueued => ("In Warteschlange", "queued", "orange"),
|
||||
EnvelopeStatus.EnvelopeSent => ("Gesendet", "sent", "orange"),
|
||||
EnvelopeStatus.EnvelopeCompletelySigned => ("Vollständig unterschrieben", "completed", "green"),
|
||||
EnvelopeStatus.EnvelopeDeleted => ("Gelöscht", "deleted", "red"),
|
||||
EnvelopeStatus.EnvelopeRejected => ("Abgelehnt", "rejected", "red"),
|
||||
EnvelopeStatus.EnvelopeWithdrawn => ("Zurückgezogen", "withdrawn", "red"),
|
||||
EnvelopeStatus.EnvelopeCreated => ("Erstellt", "created", "blue"),
|
||||
EnvelopeStatus.EnvelopeSaved => ("Gespeichert", "saved", "blue"),
|
||||
_ => ("Unbekannt", "unknown", "blue")
|
||||
};
|
||||
}
|
||||
|
||||
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
||||
{
|
||||
// Future: Add custom row coloring based on status if needed
|
||||
}
|
||||
|
||||
void OnSelectedEnvelopeChanged(object envelope)
|
||||
{
|
||||
_selectedEnvelope = envelope as EnvelopeDto;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@page "/example/documentviewer"
|
||||
|
||||
<DxDocumentViewer ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null">
|
||||
<DxDocumentViewerTabPanelSettings Width="340" />
|
||||
</DxDocumentViewer>
|
||||
@@ -1,19 +0,0 @@
|
||||
@page "/example/sender"
|
||||
@using DevExpress.DataAccess.Json;
|
||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
<DxReportDesigner ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null" AllowMDI="true" DataSources="DataSources">
|
||||
<DxReportDesignerWizardSettings UseFullscreenWizard="true" />
|
||||
</DxReportDesigner>
|
||||
|
||||
@code {
|
||||
Dictionary<string, object> DataSources = new Dictionary<string, object>();
|
||||
protected override async Task OnInitializedAsync() {
|
||||
await base.OnInitializedAsync();
|
||||
var connection = CustomDataSourceWizardJsonDataConnectionStorage.GetDefaultConnection();
|
||||
JsonDataSource jsonDataSource = new JsonDataSource();
|
||||
jsonDataSource.JsonSource = connection.GetJsonSource();
|
||||
await jsonDataSource.FillAsync();
|
||||
DataSources.Add("Northwind", jsonDataSource);
|
||||
}
|
||||
}
|
||||
@@ -1,750 +0,0 @@
|
||||
@page "/example/receiver/{EnvelopeKey}"
|
||||
@page "/example/report-viewer/{EnvelopeKey}"
|
||||
@using System.Drawing
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Drawing
|
||||
@using DevExpress.Utils
|
||||
@using DevExpress.XtraPrinting
|
||||
@using DevExpress.XtraPrinting.Drawing
|
||||
@using EnvelopeGenerator.Application.Common.Dto
|
||||
@using Microsoft.JSInterop
|
||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
||||
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
|
||||
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
|
||||
@using XRControl = DevExpress.XtraReports.UI.XRControl
|
||||
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@using Microsoft.Extensions.Options
|
||||
@using EnvelopeGenerator.ReceiverUI.Options
|
||||
@using EnvelopeGenerator.ReceiverUI.Models
|
||||
@implements IDisposable
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AnnotationService AnnotationService
|
||||
@inject IOptions<ApiOptions> AppOptions
|
||||
@inject NavigationManager Navigation
|
||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||
|
||||
<div class="receiver-page-layout">
|
||||
|
||||
<div class="receiver-signature-panel">
|
||||
|
||||
@* ?? Envelope info header ???????????????????????????????????????????????? *@
|
||||
@if (_envelopeReceiver is not null) {
|
||||
<div class="receiver-info-header">
|
||||
<div class="receiver-info-header__gradient">
|
||||
<div class="receiver-info-header__left">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" class="receiver-info-header__icon" 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"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="receiver-info-header__title">@(_envelopeReceiver.Envelope?.Title ?? "Dokument")</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||||
<div class="receiver-info-header__sender">
|
||||
Von
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) {
|
||||
<strong>@_envelopeReceiver.Envelope!.User!.FullName</strong>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||||
<span class="opacity-75"><@_envelopeReceiver.Envelope!.User!.Email></span>
|
||||
}
|
||||
· @_envelopeReceiver.Envelope?.AddedWhen.ToString("dd.MM.yyyy")
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="receiver-info-header__badges">
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) {
|
||||
<span class="receiver-info-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z"/>
|
||||
</svg>
|
||||
@_envelopeReceiver.Name
|
||||
</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.CompanyName)) {
|
||||
<span class="receiver-info-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022Z"/>
|
||||
</svg>
|
||||
@_envelopeReceiver.CompanyName
|
||||
</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.JobTitle)) {
|
||||
<span class="receiver-info-badge receiver-info-badge--muted">@_envelopeReceiver.JobTitle</span>
|
||||
}
|
||||
@{
|
||||
var docElements = _envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements;
|
||||
int sigCount = docElements?.Count() ?? _annotations.Count;
|
||||
}
|
||||
@if (sigCount > 0) {
|
||||
<span class="receiver-info-badge receiver-info-badge--accent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
@sigCount @(sigCount == 1 ? "Unterschriftsfeld" : "Unterschriftsfelder")
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) {
|
||||
<div class="receiver-info-message">@_envelopeReceiver.Envelope!.Message</div>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) {
|
||||
<div class="receiver-info-private-message">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1 flex-shrink-0" 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"/>
|
||||
</svg>
|
||||
@_envelopeReceiver.PrivateMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ?? Signature action bar ???????????????????????????????????????????????? *@
|
||||
<div class="receiver-action-bar">
|
||||
<div class="receiver-action-bar__inner">
|
||||
<button class="btn btn-sm @(_capturedSignature is not null ? "btn-outline-success" : "btn-primary")" @onclick="OpenSignaturePopupAsync">
|
||||
@if (_capturedSignature is not null) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" 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"/>
|
||||
</svg>
|
||||
<span>Unterschrift gespeichert</span>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
<span>Unterschrift erstellen</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (_annotations.Count > 0 && !SignatureApplied) {
|
||||
<div class="receiver-action-bar__progress">
|
||||
<div class="progress" style="height:5px; min-width:70px; width:90px;">
|
||||
<div class="progress-bar @(_checkedAnnotations.Count == _annotations.Count ? "bg-success" : "bg-primary")"
|
||||
style="width:@(_annotations.Count > 0 ? (_checkedAnnotations.Count * 100 / _annotations.Count) : 0)%"
|
||||
role="progressbar"></div>
|
||||
</div>
|
||||
<span class="text-muted" style="font-size:0.75rem;">
|
||||
@_checkedAnnotations.Count / @_annotations.Count
|
||||
Seite@(AnnotationPages.Count() == 1 ? "" : "n") @string.Join(",", AnnotationPages)
|
||||
</span>
|
||||
@if (_capturedSignature is null) {
|
||||
<span class="text-muted fst-italic" style="font-size:0.72rem;">Zuerst Unterschrift erstellen</span>
|
||||
} else if (_checkedAnnotations.Count == _annotations.Count) {
|
||||
<span class="text-success fw-semibold" style="font-size:0.72rem;">✓ Wird angewendet…</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SignatureValidationMessage)) {
|
||||
<span class="text-danger" style="font-size:0.78rem;">@SignatureValidationMessage</span>
|
||||
}
|
||||
@if (SignatureApplied) {
|
||||
<span class="text-success" style="font-size:0.78rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||
</svg>
|
||||
Unterschrift angewendet
|
||||
</span>
|
||||
}
|
||||
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-success" disabled="@(!SignatureApplied)" @onclick="ExportSignedPdfAsync">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" 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="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>
|
||||
PDF exportieren
|
||||
</button>
|
||||
@if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="LogoutAsync" disabled="@IsLoggingOut" title="Abmelden">
|
||||
@if (IsLoggingOut) {
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<DxPopup @bind-Visible="SignaturePopupVisible"
|
||||
HeaderText="Unterschrift erfassen"
|
||||
Width="620px"
|
||||
ShowFooter="true"
|
||||
CloseOnEscape="false"
|
||||
ShowCloseButton="false"
|
||||
CloseOnOutsideClick="false"
|
||||
Shown="OnPopupShownAsync">
|
||||
<BodyContentTemplate>
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabDraw ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabDraw)">Zeichnen</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabText ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabText)">Text</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabImage ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabImage)">Bild</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if(ActiveSignatureTab == SignatureTabDraw) {
|
||||
<p class="text-muted mb-2">Bitte unterschreiben Sie im folgenden Feld.</p>
|
||||
<canvas id="receiver-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px; touch-action: none;"></canvas>
|
||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
||||
<p class="text-muted mb-2">Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.</p>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-12 col-md-7">
|
||||
<input class="form-control" placeholder="Ihre Unterschrift" value="@TypedSignatureText" @oninput="OnTypedSignatureChanged" />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<select class="form-select" value="@TypedSignatureFont" @onchange="OnTypedSignatureFontChanged">
|
||||
@foreach(var font in TypedSignatureFonts) {
|
||||
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="receiver-typed-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
||||
} else {
|
||||
<p class="text-muted mb-2">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
|
||||
<input id="receiver-signature-image-input" class="form-control mb-2" type="file" accept="image/png,image/jpeg,image/webp" />
|
||||
<canvas id="receiver-image-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
||||
}
|
||||
|
||||
|
||||
<div class="border-top mt-3 pt-3">
|
||||
<p class="text-muted mb-2">Bitte geben Sie die folgenden Angaben ein. Das Datum wird automatisch hinzugefuegt.</p>
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="receiver-signer-name">Vor- und Nachname *</label>
|
||||
<input id="receiver-signer-name" class="form-control" value="@SignerFullName" @oninput="args => SignerFullName = args.Value?.ToString() ?? string.Empty" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="receiver-signer-position">Position</label>
|
||||
<input id="receiver-signer-position" class="form-control" value="@SignerPosition" @oninput="args => SignerPosition = args.Value?.ToString() ?? string.Empty" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="receiver-signature-place">Ort *</label>
|
||||
<input id="receiver-signature-place" class="form-control" value="@SignaturePlace" @oninput="args => SignaturePlace = args.Value?.ToString() ?? string.Empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
|
||||
<div class="text-danger mt-2">@PopupValidationMessage</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div class="d-flex gap-2 flex-wrap justify-content-end w-100">
|
||||
<button class="btn btn-outline-secondary" @onclick="RenewSignatureAsync">Unterschrift erneuern</button>
|
||||
<button class="btn btn-primary" @onclick="SaveSignatureAsync">Speichern</button>
|
||||
</div>
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
<div class="receiver-viewer-wrapper">
|
||||
@if(Report is not null) {
|
||||
<DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" Zoom="1.3" />
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
const string SignatureTabDraw = "draw";
|
||||
const string SignatureTabText = "text";
|
||||
const string SignatureTabImage = "image";
|
||||
const string DrawCanvasId = "receiver-signature-pad";
|
||||
const string TypedCanvasId = "receiver-typed-signature-pad";
|
||||
const string ImageInputId = "receiver-signature-image-input";
|
||||
const string ImageCanvasId = "receiver-image-signature-pad";
|
||||
|
||||
readonly (string Text, string Value)[] TypedSignatureFonts = {
|
||||
("Brush Script", "'Brush Script MT', cursive"),
|
||||
("Segoe Script", "'Segoe Script', cursive"),
|
||||
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
||||
("Comic Sans", "'Comic Sans MS', cursive"),
|
||||
("Cursive", "cursive")
|
||||
};
|
||||
|
||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
||||
|
||||
DxReportViewer? reportViewer;
|
||||
XtraReport? Report;
|
||||
bool SignatureApplied;
|
||||
bool SignaturePopupVisible;
|
||||
string? SignatureValidationMessage;
|
||||
string? PopupValidationMessage;
|
||||
string ActiveSignatureTab = SignatureTabDraw;
|
||||
string TypedSignatureText = string.Empty;
|
||||
string TypedSignatureFont = "'Brush Script MT', cursive";
|
||||
string SignerFullName = string.Empty;
|
||||
string SignerPosition = string.Empty;
|
||||
string SignaturePlace = string.Empty;
|
||||
int ViewerKey;
|
||||
bool IsLoggingOut;
|
||||
|
||||
IReadOnlyList<AnnotationDto> _annotations = [];
|
||||
IEnumerable<int> AnnotationPages => _annotations
|
||||
.Select(a => a.Page ?? throw new InvalidOperationException($"Annotation page is missing for annotation ID {a.Id}. Annotation details: X={a.X}, Y={a.Y}"))
|
||||
.Distinct()
|
||||
.OrderBy(p => p);
|
||||
EnvelopeReceiverDto? _envelopeReceiver;
|
||||
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
||||
SignatureCapture? _capturedSignature;
|
||||
byte[]? _basePdfBytes;
|
||||
// annotation IDs the user has checked via overlay checkboxes
|
||||
readonly HashSet<long> _checkedAnnotations = [];
|
||||
DotNetObjectReference<ReportViewer>? _dotNetRef;
|
||||
int _lastOverlayViewerKey = -1;
|
||||
|
||||
async Task LogoutAsync() {
|
||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || IsLoggingOut) return;
|
||||
IsLoggingOut = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
|
||||
// ? REDIRECT: /receiver/{key} -> /envelope/{key} (NEW PDF.js viewer)
|
||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
if (!hasAccess) {
|
||||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveSignatureTab = SignatureTabDraw;
|
||||
SignaturePopupVisible = true;
|
||||
SignatureValidationMessage = null;
|
||||
PopupValidationMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
_annotations = await AnnotationService.GetAnnotationsAsync(EnvelopeKey);
|
||||
|
||||
try {
|
||||
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey);
|
||||
} catch (HttpRequestException ex) {
|
||||
// Log error but continue - UI will handle null envelope receiver gracefully
|
||||
Console.WriteLine($"Failed to load envelope receiver: {ex.Message}");
|
||||
}
|
||||
|
||||
if (!AppOptions.Value.UsePredefinedReports && !string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
try {
|
||||
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
|
||||
if (pdfBytes is { Length: > 0 })
|
||||
_basePdfBytes = pdfBytes;
|
||||
} catch (HttpRequestException ex) {
|
||||
// Log error but continue - will use predefined report instead
|
||||
Console.WriteLine($"Failed to load document: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var initialReport = BuildFreshBaseReport();
|
||||
Report = initialReport;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (firstRender)
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
|
||||
|
||||
if (Report is not null && _annotations.Count > 0
|
||||
&& _capturedSignature is not null && !SignatureApplied
|
||||
&& _lastOverlayViewerKey != ViewerKey) {
|
||||
_lastOverlayViewerKey = ViewerKey;
|
||||
await JSRuntime.InvokeVoidAsync(
|
||||
"receiverSignature.installAnnotationCheckboxes",
|
||||
_annotations, _checkedAnnotations.ToArray(), _dotNetRef);
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task OnAnnotationToggled(long annotationId, bool isChecked) {
|
||||
if (isChecked)
|
||||
_checkedAnnotations.Add(annotationId);
|
||||
else
|
||||
_checkedAnnotations.Remove(annotationId);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (_capturedSignature is not null
|
||||
&& !SignatureApplied
|
||||
&& _annotations.Count > 0
|
||||
&& _checkedAnnotations.Count == _annotations.Count) {
|
||||
// K?sa bekleme: kullan?c? son tick'in görsel feedback'ini görsün
|
||||
await Task.Delay(400);
|
||||
await SubmitSignaturesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void OpenSignaturePopupAsync() {
|
||||
ActiveSignatureTab = SignatureTabDraw;
|
||||
SignaturePopupVisible = true;
|
||||
SignatureValidationMessage = null;
|
||||
PopupValidationMessage = null;
|
||||
}
|
||||
|
||||
async Task OnPopupShownAsync() {
|
||||
await InitializeActiveSignatureTabAsync();
|
||||
}
|
||||
|
||||
async Task SetSignatureTabAsync(string tab) {
|
||||
ActiveSignatureTab = tab;
|
||||
PopupValidationMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await Task.Delay(50);
|
||||
await InitializeActiveSignatureTabAsync();
|
||||
}
|
||||
|
||||
async Task InitializeActiveSignatureTabAsync() {
|
||||
if(ActiveSignatureTab == SignatureTabDraw) {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId);
|
||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId);
|
||||
await RenderTypedSignatureAsync();
|
||||
} else {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId);
|
||||
}
|
||||
}
|
||||
|
||||
async Task RenewSignatureAsync() {
|
||||
PopupValidationMessage = null;
|
||||
|
||||
if(ActiveSignatureTab == SignatureTabDraw) {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId);
|
||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
||||
TypedSignatureText = string.Empty;
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId);
|
||||
} else {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId);
|
||||
}
|
||||
}
|
||||
|
||||
void CloseSignaturePopup() {
|
||||
PopupValidationMessage = null;
|
||||
SignaturePopupVisible = false;
|
||||
}
|
||||
|
||||
async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
||||
TypedSignatureText = args.Value?.ToString() ?? string.Empty;
|
||||
await RenderTypedSignatureAsync();
|
||||
}
|
||||
|
||||
async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
||||
TypedSignatureFont = args.Value?.ToString() ?? TypedSignatureFont;
|
||||
await RenderTypedSignatureAsync();
|
||||
}
|
||||
|
||||
async Task RenderTypedSignatureAsync() {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, TypedSignatureText, TypedSignatureFont);
|
||||
}
|
||||
|
||||
async Task SaveSignatureAsync() {
|
||||
if (string.IsNullOrWhiteSpace(SignerFullName)) {
|
||||
PopupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(SignaturePlace)) {
|
||||
PopupValidationMessage = "Bitte geben Sie den Ort ein.";
|
||||
return;
|
||||
}
|
||||
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
||||
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
||||
PopupValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich.";
|
||||
return;
|
||||
}
|
||||
|
||||
PopupValidationMessage = null;
|
||||
SignatureValidationMessage = null;
|
||||
_capturedSignature = new(signatureDataUrl, SignerFullName.Trim(), SignerPosition.Trim(), SignaturePlace.Trim());
|
||||
|
||||
// If no annotations, apply immediately (no checkbox step needed)
|
||||
if (_annotations.Count == 0) {
|
||||
var freshReport = BuildFreshBaseReport();
|
||||
AddSignature(freshReport, _capturedSignature.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
||||
Report = freshReport;
|
||||
SignatureApplied = true;
|
||||
SignaturePopupVisible = false;
|
||||
ViewerKey++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Close popup; checkboxes will appear on the PDF via OnAfterRenderAsync
|
||||
SignaturePopupVisible = false;
|
||||
_lastOverlayViewerKey = -1; // force overlay reinstall
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
async Task SubmitSignaturesAsync() {
|
||||
if (_checkedAnnotations.Count == 0) {
|
||||
SignatureValidationMessage = "Bitte markieren Sie mindestens ein Unterschriftsfeld im Dokument.";
|
||||
return;
|
||||
}
|
||||
if (_checkedAnnotations.Count < _annotations.Count) {
|
||||
SignatureValidationMessage = $"Bitte markieren Sie alle {_annotations.Count} Unterschriftsfelder. Noch {_annotations.Count - _checkedAnnotations.Count} offen.";
|
||||
return;
|
||||
}
|
||||
|
||||
SignatureValidationMessage = null;
|
||||
var freshReport = BuildFreshBaseReport();
|
||||
foreach (var ann in _annotations)
|
||||
AddSignatureAtAnnotation(freshReport, ann, _capturedSignature!.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
||||
|
||||
Report = freshReport;
|
||||
SignatureApplied = true;
|
||||
ViewerKey++;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
async Task<string?> GetActiveSignatureDataUrlAsync() {
|
||||
if(ActiveSignatureTab == SignatureTabDraw)
|
||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
|
||||
|
||||
if(ActiveSignatureTab == SignatureTabText) {
|
||||
await RenderTypedSignatureAsync();
|
||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
|
||||
}
|
||||
|
||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
|
||||
}
|
||||
|
||||
async Task ExportSignedPdfAsync() {
|
||||
if(!SignatureApplied || Report is null) {
|
||||
SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SignatureValidationMessage = null;
|
||||
await reportViewer!.ExportToAsync(ExportFormat.Pdf);
|
||||
} catch(Exception) {
|
||||
SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.";
|
||||
}
|
||||
}
|
||||
|
||||
XtraReport BuildFreshBaseReport() {
|
||||
if (_basePdfBytes is { Length: > 0 }) {
|
||||
var report = new XtraReport();
|
||||
var detail = new DevExpress.XtraReports.UI.DetailBand();
|
||||
report.Bands.Add(detail);
|
||||
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
||||
return report;
|
||||
}
|
||||
return CreateReportInstance();
|
||||
}
|
||||
|
||||
static void AddAnnotationPlaceholders(XtraReport report, IReadOnlyList<AnnotationDto> annotations) {
|
||||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||||
if (bottomMargin is null) {
|
||||
bottomMargin = new BottomMarginBand();
|
||||
report.Bands.Add(bottomMargin);
|
||||
}
|
||||
|
||||
const float sigWidth = 230F;
|
||||
const float sigHeight = 154F;
|
||||
const float bottomPad = 6F;
|
||||
const float defaultTopPad = 8F;
|
||||
const float maxBandHeight = 210F;
|
||||
|
||||
float requiredHeight = defaultTopPad + sigHeight + bottomPad;
|
||||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
||||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - sigHeight);
|
||||
|
||||
foreach (var ann in annotations) {
|
||||
float sigX = (float)(ann.X);
|
||||
var annotId = ann.Id.ToString();
|
||||
|
||||
var placeholder = new XRLabel {
|
||||
Name = $"receiverSignaturePlaceholder_{annotId}",
|
||||
Text = "\u270e Bitte unterschreiben",
|
||||
BoundsF = new RectangleF(sigX, topPad, sigWidth, sigHeight),
|
||||
Borders = BorderSide.All,
|
||||
BorderColor = System.Drawing.Color.FromArgb(230, 81, 0),
|
||||
BackColor = System.Drawing.Color.FromArgb(30, 255, 236, 153),
|
||||
Font = new DXFont("Open Sans", 10F, DXFontStyle.Regular),
|
||||
ForeColor = System.Drawing.Color.FromArgb(94, 38, 0),
|
||||
TextAlignment = TextAlignment.MiddleCenter
|
||||
};
|
||||
|
||||
bottomMargin.Controls.Add(placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
XtraReport CreateReportInstance() {
|
||||
return ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport)
|
||||
? savedReport
|
||||
: PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
|
||||
}
|
||||
|
||||
static void AddSignature(XtraReport report, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
||||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
||||
using var imageStream = new MemoryStream(imageBytes);
|
||||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
||||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||||
|
||||
if(bottomMargin is null) {
|
||||
bottomMargin = new BottomMarginBand();
|
||||
report.Bands.Add(bottomMargin);
|
||||
}
|
||||
|
||||
RemoveExistingSignature(bottomMargin);
|
||||
|
||||
// Layout constants
|
||||
const float sigX = 390F;
|
||||
const float sigWidth = 230F;
|
||||
const float sigImgHeight = 70F;
|
||||
const float infoHeight = 65F; // up to 4 lines at 8pt
|
||||
const float innerGap = 5F;
|
||||
const float bottomPad = 6F;
|
||||
const float defaultTopPad = 8F;
|
||||
const float maxBandHeight = 210F;
|
||||
|
||||
float requiredHeight = defaultTopPad + sigImgHeight + innerGap + infoHeight + bottomPad;
|
||||
|
||||
// Grow band if needed, but cap at maxBandHeight to avoid overlapping page content
|
||||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
||||
|
||||
// If band is tighter than required, compress top padding so content still fits
|
||||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - infoHeight - innerGap - sigImgHeight);
|
||||
|
||||
float imageY = topPad;
|
||||
float labelY = imageY + sigImgHeight + innerGap;
|
||||
|
||||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
||||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
||||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
||||
|
||||
var signature = new XRPictureBox {
|
||||
Name = "receiverSignatureImage",
|
||||
ImageSource = imageSource,
|
||||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
||||
Sizing = ImageSizeMode.ZoomImage,
|
||||
Borders = BorderSide.Bottom,
|
||||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87)
|
||||
};
|
||||
|
||||
var signatureLabel = new XRLabel {
|
||||
Name = "receiverSignatureLabel",
|
||||
Text = signatureInformation,
|
||||
Multiline = true,
|
||||
BoundsF = new RectangleF(sigX, labelY, sigWidth, infoHeight),
|
||||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
||||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||||
TextAlignment = TextAlignment.TopLeft
|
||||
};
|
||||
|
||||
bottomMargin.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
||||
}
|
||||
|
||||
static void RemoveExistingSignature(BottomMarginBand bottomMargin) {
|
||||
var controls = bottomMargin.Controls
|
||||
.Cast<XRControl>()
|
||||
.Where(control => control.Name is "receiverSignatureLabel" or "receiverSignatureImage")
|
||||
.ToArray();
|
||||
|
||||
foreach(var control in controls)
|
||||
bottomMargin.Controls.Remove(control);
|
||||
}
|
||||
|
||||
static void AddSignatureAtAnnotation(XtraReport report, AnnotationDto? annotation, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
||||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
||||
using var imageStream = new MemoryStream(imageBytes);
|
||||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
||||
|
||||
var detail = report.Bands.OfType<DevExpress.XtraReports.UI.DetailBand>().FirstOrDefault();
|
||||
if (detail is null) return;
|
||||
|
||||
var annotId = annotation?.Id.ToString() ?? "0";
|
||||
RemoveExistingSignatureById(detail, annotId);
|
||||
|
||||
const float sigWidth = 230F;
|
||||
const float sigImgHeight = 70F;
|
||||
const float infoHeight = 48.75F;
|
||||
const float innerGap = 5F;
|
||||
|
||||
float sigX = (float)(annotation?.X ?? 390.0);
|
||||
float imageY = (float)(annotation?.Y ?? 900.0);
|
||||
float labelY = imageY + sigImgHeight + innerGap;
|
||||
int targetPage = annotation?.Page ?? 1;
|
||||
|
||||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
||||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
||||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
||||
|
||||
var signature = new XRPictureBox {
|
||||
Name = $"receiverSignatureImage_{annotId}",
|
||||
ImageSource = imageSource,
|
||||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
||||
Sizing = ImageSizeMode.ZoomImage,
|
||||
Borders = BorderSide.Bottom,
|
||||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||||
BackColor = Color.FromArgb(219, 219, 219)
|
||||
};
|
||||
|
||||
var signatureLabel = new XRLabel {
|
||||
Name = $"receiverSignatureLabel_{annotId}",
|
||||
Text = signatureInformation,
|
||||
Multiline = true,
|
||||
BoundsF = new RectangleF(sigX, labelY - 5, sigWidth, infoHeight),
|
||||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
||||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||||
TextAlignment = TextAlignment.TopCenter,
|
||||
BackColor = Color.FromArgb(219, 219, 219)
|
||||
};
|
||||
|
||||
// Show each control only on the target page using an independent print counter
|
||||
int sigPrintCount = 0;
|
||||
signature.BeforePrint += (_, e) => {
|
||||
sigPrintCount++;
|
||||
e.Cancel = sigPrintCount != targetPage;
|
||||
};
|
||||
|
||||
int lblPrintCount = 0;
|
||||
signatureLabel.BeforePrint += (_, e) => {
|
||||
lblPrintCount++;
|
||||
e.Cancel = lblPrintCount != targetPage;
|
||||
};
|
||||
|
||||
detail.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
||||
}
|
||||
|
||||
static void RemoveExistingSignatureById(DevExpress.XtraReports.UI.DetailBand detail, string annotId) {
|
||||
var controls = detail.Controls
|
||||
.Cast<XRControl>()
|
||||
.Where(c => c.Name == $"receiverSignatureImage_{annotId}" || c.Name == $"receiverSignatureLabel_{annotId}")
|
||||
.ToArray();
|
||||
foreach (var c in controls)
|
||||
detail.Controls.Remove(c);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_dotNetRef?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
@page "/example/test"
|
||||
@using System.Drawing
|
||||
@using DevExpress.Drawing
|
||||
@using DevExpress.Utils
|
||||
@using DevExpress.XtraPrinting
|
||||
@using DevExpress.XtraPrinting.Drawing
|
||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
||||
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
|
||||
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
|
||||
@using XRControl = DevExpress.XtraReports.UI.XRControl
|
||||
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
|
||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||
@inject HttpClient Http
|
||||
@using System;
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||
|
||||
<div class="receiver-page-layout">
|
||||
|
||||
<div class="receiver-viewer-wrapper">
|
||||
<embed class="w-100 h-50" src="/docs/Document.pdf" type="application/pdf" />
|
||||
@if (PdfBytes is { Length: > 0 }) {
|
||||
<DxPdfViewer DocumentContent="PdfBytes" CssClass="w-100 h-50" />
|
||||
}
|
||||
else{
|
||||
<div>Not found</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
const string SignatureTabDraw = "draw";
|
||||
const string SignatureTabText = "text";
|
||||
const string SignatureTabImage = "image";
|
||||
const string DrawCanvasId = "receiver-signature-pad";
|
||||
const string TypedCanvasId = "receiver-typed-signature-pad";
|
||||
const string ImageInputId = "receiver-signature-image-input";
|
||||
const string ImageCanvasId = "receiver-image-signature-pad";
|
||||
|
||||
readonly (string Text, string Value)[] TypedSignatureFonts = {
|
||||
("Brush Script", "'Brush Script MT', cursive"),
|
||||
("Segoe Script", "'Segoe Script', cursive"),
|
||||
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
||||
("Comic Sans", "'Comic Sans MS', cursive"),
|
||||
("Cursive", "cursive")
|
||||
};
|
||||
|
||||
[Parameter] public string? EnvelopeKey { get; set; }
|
||||
|
||||
DxReportViewer? reportViewer;
|
||||
XtraReport? Report;
|
||||
string PdfViewerUrl = string.Empty;
|
||||
byte[]? PdfBytes;
|
||||
byte[]? SignedPdfBytes;
|
||||
bool SignatureApplied;
|
||||
bool SignaturePopupVisible;
|
||||
string? PopupValidationMessage;
|
||||
string ActiveSignatureTab = SignatureTabDraw;
|
||||
string TypedSignatureText = string.Empty;
|
||||
string TypedSignatureFont = "'Brush Script MT', cursive";
|
||||
string SignerFullName = string.Empty;
|
||||
string SignerPosition = string.Empty;
|
||||
string SignaturePlace = string.Empty;
|
||||
int ViewerKey;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
PdfBytes = await Http.GetByteArrayAsync("/docs/Document.pdf");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
@page "/"
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
<div class="home-page-wrapper">
|
||||
|
||||
<div class="home-hero-header">
|
||||
<div class="home-hero-header__inner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" 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"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h1 class="home-hero-header__title">SignFlow</h1>
|
||||
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-content">
|
||||
<div class="home-card card shadow border-0">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
<p class="text-muted mb-4" style="font-size: 0.92rem; line-height: 1.7; text-align: justify; text-align-last: left; min-height: calc(0.92rem * 1.7 * 9);">
|
||||
<span id="home-description"></span>
|
||||
</p>
|
||||
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||
<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">
|
||||
<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>
|
||||
Sicherer Zugang
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
Digitale Unterschrift
|
||||
</div>
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
PDF-Export
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private const string HomePageDescription =
|
||||
"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.";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
@page "/envelope/login/{EnvelopeKey}"
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
<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="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
|
||||
<div class="mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" 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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
||||
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
|
||||
<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.
|
||||
</p>
|
||||
|
||||
@if (LoginResult == EnvelopeLoginResult.NotFound) {
|
||||
<div class="alert alert-warning d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<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>
|
||||
<div>
|
||||
<strong>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>
|
||||
</div>
|
||||
</div>
|
||||
} else if (LoginResult == EnvelopeLoginResult.InvalidCode) {
|
||||
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<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>
|
||||
<div>
|
||||
<strong>Ungültiger Zugangscode.</strong><br />
|
||||
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||
</div>
|
||||
</div>
|
||||
} else if (LoginResult == EnvelopeLoginResult.Error) {
|
||||
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<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>
|
||||
<div>
|
||||
<strong>Serverfehler.</strong><br />
|
||||
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-medium" for="login-access-code">
|
||||
Zugangscode
|
||||
<span class="text-danger ms-1">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
||||
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input id="login-access-code"
|
||||
type="@(ShowCode ? "text" : "password")"
|
||||
class="form-control border-start-0 border-end-0 @(LoginResult == EnvelopeLoginResult.InvalidCode ? "is-invalid" : null)"
|
||||
placeholder="Zugangscode eingeben"
|
||||
@bind="AccessCode"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="OnKeyDownAsync"
|
||||
disabled="@IsLoading"
|
||||
autocomplete="one-time-code" />
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary border-start-0"
|
||||
style="border-left: none;"
|
||||
tabindex="-1"
|
||||
@onclick="() => ShowCode = !ShowCode">
|
||||
@if (ShowCode) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
||||
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
|
||||
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
|
||||
</svg>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100 py-2 fw-medium"
|
||||
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||
@onclick="SubmitAsync"
|
||||
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
||||
@if (IsLoading) {
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
<span>Überprüfen …</span>
|
||||
} else {
|
||||
<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"/>
|
||||
</svg>
|
||||
<span>Dokument öffnen</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<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.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
||||
|
||||
string AccessCode = string.Empty;
|
||||
bool ShowCode;
|
||||
bool IsLoading;
|
||||
EnvelopeLoginResult? LoginResult;
|
||||
|
||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
||||
if (e.Key == "Enter")
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
async Task SubmitAsync() {
|
||||
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
||||
|
||||
IsLoading = true;
|
||||
LoginResult = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
||||
|
||||
if (result == EnvelopeLoginResult.Success) {
|
||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
|
||||
LoginResult = result;
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
@page "/sender/login"
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
<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="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
|
||||
<div class="mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
|
||||
<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>
|
||||
</div>
|
||||
<h5 class="mb-0 fw-semibold">Sender Anmeldung</h5>
|
||||
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang zum Sender-Dashboard</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
|
||||
<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.
|
||||
</p>
|
||||
|
||||
@if (LoginResult == SenderLoginResult.InvalidCredentials) {
|
||||
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<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>
|
||||
<div>
|
||||
<strong>Ungültige Anmeldedaten.</strong><br />
|
||||
<span style="font-size:0.85rem;">Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||
</div>
|
||||
</div>
|
||||
} else if (LoginResult == SenderLoginResult.Error) {
|
||||
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<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>
|
||||
<div>
|
||||
<strong>Serverfehler.</strong><br />
|
||||
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium" for="login-username">
|
||||
Benutzername
|
||||
<span class="text-danger ms-1">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input id="login-username"
|
||||
type="text"
|
||||
class="form-control @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
||||
placeholder="Benutzername eingeben"
|
||||
@bind="Username"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="OnKeyDownAsync"
|
||||
disabled="@IsLoading"
|
||||
autocomplete="username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-medium" for="login-password">
|
||||
Passwort
|
||||
<span class="text-danger ms-1">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" 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"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input id="login-password"
|
||||
type="@(ShowPassword ? "text" : "password")"
|
||||
class="form-control border-start-0 border-end-0 @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
||||
placeholder="Passwort eingeben"
|
||||
@bind="Password"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="OnKeyDownAsync"
|
||||
disabled="@IsLoading"
|
||||
autocomplete="current-password" />
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary border-start-0"
|
||||
style="border-left: none;"
|
||||
tabindex="-1"
|
||||
@onclick="() => ShowPassword = !ShowPassword">
|
||||
@if (ShowPassword) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
||||
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
|
||||
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
|
||||
</svg>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100 py-2 fw-medium"
|
||||
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||
@onclick="SubmitAsync"
|
||||
disabled="@(IsLoading || string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))">
|
||||
@if (IsLoading) {
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
<span>Anmelden …</span>
|
||||
} else {
|
||||
<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="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>
|
||||
<span>Anmelden</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<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.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
string Username = string.Empty;
|
||||
string Password = string.Empty;
|
||||
bool ShowPassword;
|
||||
bool IsLoading;
|
||||
SenderLoginResult? LoginResult;
|
||||
|
||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
||||
if (e.Key == "Enter")
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
async Task SubmitAsync() {
|
||||
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password) || IsLoading) return;
|
||||
|
||||
IsLoading = true;
|
||||
LoginResult = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim());
|
||||
|
||||
if (result == SenderLoginResult.Success) {
|
||||
Navigation.NavigateTo("/sender", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
|
||||
LoginResult = result;
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
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