Compare commits
51 Commits
b6ec5307b6
...
bugfix/dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 7456babe0d | |||
| 71e375d6ea | |||
| 05f64e2b61 | |||
| ed17852542 | |||
| 9947774ba8 | |||
| c6c1decd2a | |||
| 0fdaa1a38d | |||
| 5d66de9f32 | |||
| 6fe99d0cd0 | |||
| 45018d04b1 | |||
| b5af3e61ed | |||
| 314608f27f | |||
| ba9f233993 | |||
| 9d962708c4 | |||
| c93a056ca5 | |||
| a88a26c248 | |||
| 1e963ea215 | |||
| 02b857382c | |||
| ca4ec7cb6f | |||
| f2356b3ce4 | |||
| d61fe79613 | |||
| 714cb9555f | |||
| 315a022cb8 | |||
| 746635979b | |||
| 31548728cd | |||
| 06c8af2ed8 | |||
| 9f57baf2e5 | |||
| 73d793f0a0 | |||
| 65bb68feef | |||
| c5e97ee30b | |||
| 3a4f449b59 | |||
| 6ca7767e4d | |||
| 4237f0a815 | |||
| 3302be9348 | |||
| 4572e20c51 | |||
| b3a70d7259 | |||
| bb81920d44 | |||
| 3b66de0691 | |||
| 9f6004ba8c | |||
| ef246bae32 | |||
| e4ebb29969 | |||
| 83cdb9dfe9 | |||
| c5db676e01 | |||
| 95c8e15887 | |||
| 561b844e59 | |||
| 011960be75 | |||
| 151c785af9 | |||
| fa354a05cc | |||
| 1326407462 | |||
| a3c653ddb3 | |||
| 8d736cdc5e |
@@ -1,4 +1,4 @@
|
|||||||
# EnvelopeGenerator — AI Context Reference
|
# EnvelopeGenerator — AI Context Reference
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Digital document signing system with **unified Blazor Auto (Server+WASM hybrid) frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
|
Digital document signing system with **unified Blazor Auto (Server+WASM hybrid) frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
|
||||||
@@ -125,6 +125,71 @@ Client ? WebUI:XXXX (Blazor Auto)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Localization & Culture Management
|
||||||
|
|
||||||
|
**Current Architecture:** Blazor WebAssembly (client-side culture management)
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
**Culture Storage:**
|
||||||
|
- Culture preference stored in browser's `localStorage` (key: `AppCulture`)
|
||||||
|
- Managed by `CultureService.cs` (ReceiverUI/Services)
|
||||||
|
- Supported cultures: `de-DE`, `en-US`, `fr-FR`
|
||||||
|
|
||||||
|
**Culture Initialization:**
|
||||||
|
- **Location:** `Program.cs` (lines 53-57)
|
||||||
|
- Sets `CultureInfo.DefaultThreadCurrentCulture/UICulture` **before** app runs
|
||||||
|
- **WASM-Safe:** Each user has isolated browser instance
|
||||||
|
|
||||||
|
**Language Selector:**
|
||||||
|
- **Component:** `LanguageSelector.razor` (ReceiverUI/Shared)
|
||||||
|
- Displays flag icon + language name
|
||||||
|
- Changes culture via `CultureService.SetCultureAsync()`
|
||||||
|
- Navigates with `forceLoad: false` (smooth transition, no page reload)
|
||||||
|
|
||||||
|
### ⚠️ MIGRATION WARNING: Blazor Server/Auto
|
||||||
|
|
||||||
|
**Current approach is WASM-specific and will break in Server/Auto render modes!**
|
||||||
|
|
||||||
|
**Why it breaks:**
|
||||||
|
- `Program.cs:53-57` sets **global** `DefaultThreadCurrentCulture`
|
||||||
|
- In Server/Auto, one app instance serves **all users**
|
||||||
|
- User A selects German → User B sees German too (shared state)
|
||||||
|
- Thread-safety issues and culture conflicts
|
||||||
|
|
||||||
|
**Migration Checklist (when moving to Server/Auto):**
|
||||||
|
|
||||||
|
1. **Remove global culture initialization** from `Program.cs` (lines 53-57)
|
||||||
|
- See detailed warning comment in the code
|
||||||
|
|
||||||
|
2. **Add RequestLocalizationMiddleware** (Server-side approach):
|
||||||
|
```csharp
|
||||||
|
app.UseRequestLocalization(options => {
|
||||||
|
options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" };
|
||||||
|
options.SupportedUICultures = options.SupportedCultures;
|
||||||
|
options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **OR** Use **per-circuit culture** (Blazor Server approach):
|
||||||
|
- Store culture in circuit-scoped service
|
||||||
|
- Use `CascadingParameter` to distribute to components
|
||||||
|
- See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
|
||||||
|
|
||||||
|
4. **Update `LanguageSelector.razor`:**
|
||||||
|
- Remove manual `CultureInfo.DefaultThreadCurrentCulture` assignment
|
||||||
|
- Use middleware/circuit culture provider instead
|
||||||
|
|
||||||
|
5. **Update `CultureService.cs`:**
|
||||||
|
- Integrate with Server-side culture provider
|
||||||
|
- May need to store in cookies instead of localStorage
|
||||||
|
|
||||||
|
**References:**
|
||||||
|
- Microsoft Docs: [Blazor Globalization/Localization](https://learn.microsoft.com/aspnet/core/blazor/globalization-localization)
|
||||||
|
- Current implementation: `Program.cs`, `CultureService.cs`, `LanguageSelector.razor`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Key Files & Routes
|
## Key Files & Routes
|
||||||
|
|
||||||
### Client-Side Pages (WebUI.Client)
|
### Client-Side Pages (WebUI.Client)
|
||||||
@@ -156,7 +221,7 @@ Client ? WebUI:XXXX (Blazor Auto)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Coordinate System — CRITICAL
|
## Coordinate System — CRITICAL
|
||||||
|
|
||||||
**Database Format:** INCHES (GdPicture14 native)
|
**Database Format:** INCHES (GdPicture14 native)
|
||||||
**Origin:** Top-left corner
|
**Origin:** Top-left corner
|
||||||
@@ -185,7 +250,7 @@ Client ? WebUI:XXXX (Blazor Auto)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## EnvelopeReceiver — PDF.js Viewer & Signing
|
## EnvelopeReceiver — PDF.js Viewer & Signing
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
**Route:** `/envelope/{EnvelopeKey}`
|
||||||
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality
|
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality
|
||||||
@@ -228,7 +293,7 @@ window.pdfViewer = {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Signature Workflow — EnvelopeReceiver
|
## Signature Workflow — EnvelopeReceiver
|
||||||
|
|
||||||
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
||||||
|
|
||||||
@@ -281,9 +346,9 @@ public sealed record SignatureCaptureDto {
|
|||||||
### API Endpoints
|
### API Endpoints
|
||||||
**Controller:** `API/Controllers/CacheController.cs`
|
**Controller:** `API/Controllers/CacheController.cs`
|
||||||
|
|
||||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
||||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
||||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
||||||
|
|
||||||
**Cache Key Format:**
|
**Cache Key Format:**
|
||||||
```
|
```
|
||||||
@@ -349,7 +414,7 @@ public async Task<SenderLoginResult> LoginSenderAsync(string username, string pa
|
|||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
- `200 OK` ? Cookie set, redirect to `/sender`
|
- `200 OK` ? Cookie set, redirect to `/sender`
|
||||||
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
||||||
- Other ? Show error: "Serverfehler"
|
- Other ? Show error: "Serverfehler"
|
||||||
|
|
||||||
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
||||||
@@ -407,7 +472,7 @@ public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, st
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
## Mistakes History — Do NOT Repeat
|
||||||
|
|
||||||
| Mistake | Why Wrong |
|
| Mistake | Why Wrong |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -426,9 +491,9 @@ public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, st
|
|||||||
|
|
||||||
### Deprecated Projects
|
### Deprecated Projects
|
||||||
**DO NOT USE:**
|
**DO NOT USE:**
|
||||||
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
|
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
|
||||||
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
|
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
|
||||||
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
||||||
|
|
||||||
### Legacy Projects (VB.NET)
|
### Legacy Projects (VB.NET)
|
||||||
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
||||||
@@ -453,8 +518,8 @@ Proves database uses INCHES natively.
|
|||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
### When working with coordinates:
|
### When working with coordinates:
|
||||||
1. **Database ? UI:** INCHES × 72 = PDF Points
|
1. **Database ? UI:** INCHES × 72 = PDF Points
|
||||||
2. **UI ? Display:** Points × scale = Pixels
|
2. **UI ? Display:** Points × scale = Pixels
|
||||||
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
||||||
|
|
||||||
### When adding features:
|
### When adding features:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
|||||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</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.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
public async Task<IActionResult> Logout()
|
public async Task<IActionResult> Logout()
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
|||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[HttpGet("check")]
|
[HttpGet("check")]
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
public IActionResult Check(string? role = null)
|
public IActionResult Check(string? role = null)
|
||||||
=> role is not null && !User.IsInRole(role)
|
=> role is not null && !User.IsInRole(role)
|
||||||
? Unauthorized()
|
? Unauthorized()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -13,7 +14,7 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class SignatureController : ControllerBase
|
public class DocReceiverElementController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SignatureController(IMediator mediator)
|
public DocReceiverElementController(IMediator mediator)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<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="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
<PackageReference Include="itext" Version="8.0.5" />
|
<PackageReference Include="itext" Version="8.0.5" />
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using EnvelopeGenerator.Application;
|
|||||||
using DigitalData.Auth.Client;
|
using DigitalData.Auth.Client;
|
||||||
using DigitalData.Core.Abstractions;
|
using DigitalData.Core.Abstractions;
|
||||||
using EnvelopeGenerator.API.Models;
|
using EnvelopeGenerator.API.Models;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
using EnvelopeGenerator.API.Middleware;
|
using EnvelopeGenerator.API.Middleware;
|
||||||
@@ -22,6 +21,7 @@ using NLog.Web;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using DigitalData.Auth.Claims;
|
using DigitalData.Auth.Claims;
|
||||||
using EnvelopeGenerator.API;
|
using EnvelopeGenerator.API;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
logger.Info("Logging initialized!");
|
logger.Info("Logging initialized!");
|
||||||
@@ -44,7 +44,11 @@ try
|
|||||||
|
|
||||||
var deferredProvider = new DeferredServiceProvider();
|
var deferredProvider = new DeferredServiceProvider();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||||
|
});
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||||
|
|
||||||
@@ -238,8 +242,9 @@ try
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorizationBuilder()
|
builder.Services.AddAuthorizationBuilder()
|
||||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
|
||||||
|
.RequireRole(Role.Sender, Role.Receiver.Full)
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
||||||
.AddPolicy(AuthPolicy.Sender, policy => policy
|
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||||
.RequireRole(Role.Sender)
|
.RequireRole(Role.Sender)
|
||||||
.AddAuthenticationSchemes(AuthScheme.Sender))
|
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "sender",
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing configuration settings.
|
/// Data Transfer Object representing configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ConfigDto
|
public class ConfigDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocReceiverElementDto : IDocReceiverElement
|
public class DocReceiverElementDto : IDocReceiverElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentDto
|
public class DocumentDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentStatusDto
|
public class DocumentStatusDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using DigitalData.UserManager.Application.DTOs.User;
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeDto : IEnvelope
|
public record EnvelopeDto : IEnvelope
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,5 +128,5 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ReceiverDto>? Receivers { get; set; }
|
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverDto
|
public record EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="DateValid"></param>
|
/// <param name="DateValid"></param>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||||
DateTime DateValid)
|
DateTime DateValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
@@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||||
/// Contains information about the receiver, associated envelope, and audit details.
|
/// Contains information about the receiver, associated envelope, and audit details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyDto
|
public class EnvelopeReceiverReadOnlyDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeTypeDto
|
public class EnvelopeTypeDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record SmsResponse
|
public record SmsResponse
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ReceiverDto
|
public class ReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the AutoMapper profile configuration for mapping between
|
/// Represents the AutoMapper profile configuration for mapping between
|
||||||
@@ -26,15 +28,15 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver)));
|
CreateMap<Envelope, EnvelopeDto>();
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
CreateMap<Receiver, ReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||||
|
|
||||||
// DTO to Entity mappings
|
// DTO to Entity mappings
|
||||||
@@ -47,13 +49,13 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||||
CreateMap<EnvelopeDto, Envelope>();
|
CreateMap<EnvelopeDto, Envelope>();
|
||||||
CreateMap<DocumentDto, Document>();
|
CreateMap<DocumentDto, Document>();
|
||||||
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
||||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||||
.MapAddedWhen();
|
.MapAddedWhen();
|
||||||
|
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyActive { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyCompleted { get; init; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage des Include des Umschlags
|
/// Abfrage des Include des Umschlags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -132,6 +142,12 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(request is { OnlyActive: true })
|
||||||
|
query = query.Where(e => Status.Active.Contains(e.Status));
|
||||||
|
|
||||||
|
if (request is { OnlyCompleted: true })
|
||||||
|
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a receiver's information.
|
/// Data Transfer Object for updating a receiver's information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class UpdateReceiverCommand
|
public class UpdateReceiverCommand
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -397,4 +397,412 @@ public static class Extensions
|
|||||||
/// <param name="suffix"></param>
|
/// <param name="suffix"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
||||||
|
|
||||||
|
// Sender-side UI resources
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
|
||||||
}
|
}
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Bestätigungen</value>
|
<value>Bestätigungen</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Neuer Umschlag</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag laden</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag zurückrufen/löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Daten Aktualisieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Aktualisiert: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Dokument anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger kontaktieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Umschlag-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Öffne Log Verzeichnis</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Ergebnisbericht anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Einladung manuell versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Anrede</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Unterschrieben wann</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Benutzer</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Typ</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titel</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Erstellt am</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Zuletzt geändert am</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Offene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Abgeschlossene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Faktor Eigenschaften</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Telefonnummer</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Datei hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Dateien zusammenführen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Datei löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Datei anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Felder bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Daten bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Speichern</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Abbrechen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Signatur hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Signatur löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Sprache</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode verwenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Faktor-Authentifizierung aktiviert</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Zertifizierungstyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Ersteller</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Erinnerungs-E-Mails senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Erste Erinnerung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Erinnerungsintervall (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Läuft ab nach (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Ablaufwarnung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Nachricht</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Umschlagtyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Alle Optionen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Grund für Löschung</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Bitte geben Sie einen Grund an</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>New Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Load Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Delete Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Reload Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Refreshed: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Show Document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contact Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Envelope-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Open Log Directory</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Show Results Report</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Send Invitation Again</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Salutation</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signed When</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>User</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Title</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Created On</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Last Modified</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Open Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Completed Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Send Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Factor Properties</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Phone Number</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Add Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Delete Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Add File</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Merge Files</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Delete File</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Show File</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Edit Fields</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Edit Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Save</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Send Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Cancel</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Add Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Delete Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Use Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Factor Authentication Enabled</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Certification Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Final Email to Creator</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Final Email to Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Send Reminder Emails</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>First Reminder (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Reminder Interval (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expires After (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Expiry Warning (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Envelope Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>All Options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Deletion Reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Please provide a reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Nouvelle enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Charger l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Supprimer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Actualiser les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Actualisé : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Afficher le document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contacter le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>ID d'enveloppe : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Ouvrir le répertoire des logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Afficher le rapport de résultats</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>E-mail de support</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Renvoyer l'invitation</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Exporter</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Formule de politesse</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signé quand</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Utilisateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titre</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Créé le</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Dernière modification</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes ouvertes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes terminées</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Envoyer le code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>Propriétés 2-facteurs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Nom</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Numéro de téléphone</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Ajouter un destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Supprimer le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Ajouter un fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Fusionner les fichiers</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Supprimer le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Afficher le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Modifier les champs</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Modifier les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Enregistrer</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Envoyer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Annuler</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Ajouter une signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Supprimer la signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Langue</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Utiliser un code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>Authentification à 2 facteurs activée</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Type de certification</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>E-mail final au créateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>E-mail final aux destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Envoyer des e-mails de rappel</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Premier rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Intervalle de rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expire après (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Avertissement d'expiration (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Type d'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Toutes les options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Motif de suppression</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Veuillez indiquer une raison</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Statut</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
{
|
{
|
||||||
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/
|
// http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/
|
||||||
public enum EnvelopeStatus
|
public enum EnvelopeStatus
|
||||||
{
|
{
|
||||||
Invalid = 0,
|
Invalid = 0,
|
||||||
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
EnvelopeStatus.EnvelopeCreated,
|
EnvelopeStatus.EnvelopeCreated,
|
||||||
EnvelopeStatus.DocumentMod_Rotation
|
EnvelopeStatus.DocumentMod_Rotation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsActive())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsCompleted())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EnvelopeStatusExtensions
|
||||||
|
{
|
||||||
|
public static bool IsActive(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsCompleted(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#if NET
|
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure
|
|
||||||
{
|
|
||||||
public class EGDbContextFactory : IDesignTimeDbContextFactory<EGDbContext>
|
|
||||||
{
|
|
||||||
public EGDbContext CreateDbContext(string[] args)
|
|
||||||
{
|
|
||||||
var config = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
|
||||||
.AddJsonFile("appsettings.migration.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// create DbContextOptions
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<EGDbContext>();
|
|
||||||
optionsBuilder.UseSqlServer(config.GetConnectionString("Default"));
|
|
||||||
|
|
||||||
// create DbTriggerParams
|
|
||||||
var triggerLists = config.GetSection("DbTriggerParams").Get<Dictionary<string, List<string>>>();
|
|
||||||
var dbTriggerParams = new DbTriggerParams();
|
|
||||||
if (triggerLists is not null)
|
|
||||||
foreach (var triggerList in triggerLists)
|
|
||||||
{
|
|
||||||
if (triggerList.Value.Count == 0)
|
|
||||||
continue; // Skip empty trigger lists
|
|
||||||
|
|
||||||
var tableName = triggerList.Key;
|
|
||||||
|
|
||||||
dbTriggerParams[tableName] = new List<string>();
|
|
||||||
|
|
||||||
foreach (var trigger in triggerList.Value)
|
|
||||||
{
|
|
||||||
dbTriggerParams[tableName].Add(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams));
|
|
||||||
dbContext.IsMigration = true;
|
|
||||||
return dbContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<Router AppAssembly="@typeof(Program).Assembly">
|
@using System.Globalization
|
||||||
|
|
||||||
|
<Router AppAssembly="@typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
</Found>
|
</Found>
|
||||||
|
|||||||
@@ -24,11 +24,13 @@
|
|||||||
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.8" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.8" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.8" />
|
||||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.8" />
|
||||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
<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.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||||
@@ -40,6 +42,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Properties\PublishProfiles\" />
|
<Folder Include="Properties\PublishProfiles\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a pre-assigned signature annotation position on a specific page.
|
|
||||||
/// <br/><br/>
|
|
||||||
/// <b>Coordinate unit (X, Y):</b> Inches (GdPicture14 native unit),
|
|
||||||
/// origin at the <b>top-left</b> corner of the page, both axes increase downward/rightward.
|
|
||||||
/// <br/><br/>
|
|
||||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
|
||||||
/// Convert: <c>xDX = xInches * 100.0</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Conversion to PDF Points:</b> Multiply by 72 (1 inch = 72 points).
|
|
||||||
/// Convert: <c>xPt = xInches * 72.0</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Y-axis for PDF (bottom-left origin):</b> Flip required for iText7.
|
|
||||||
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use SignatureDto with SignatureService.")]
|
|
||||||
public record AnnotationDto
|
|
||||||
{
|
|
||||||
/// <summary>Unique identifier of the annotation.</summary>
|
|
||||||
public long Id { get; init; }
|
|
||||||
|
|
||||||
/// <summary>1-based page number within the document.</summary>
|
|
||||||
public int Page { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Horizontal position in INCHES from the left edge of the page.</summary>
|
|
||||||
public double X { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Vertical position in INCHES from the top edge of the page.</summary>
|
|
||||||
public double Y { get; init; }
|
|
||||||
}
|
|
||||||
39
EnvelopeGenerator.ReceiverUI/Models/EnvelopeDto.cs
Normal file
39
EnvelopeGenerator.ReceiverUI/Models/EnvelopeDto.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
@inject IOptions<ApiOptions> AppOptions
|
@inject IOptions<ApiOptions> AppOptions
|
||||||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject SignatureService SignatureService
|
@inject DocReceiverElementService SignatureService
|
||||||
@inject SignatureCacheService SignatureCacheService
|
@inject SignatureCacheService SignatureCacheService
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||||
|
|||||||
@@ -1,7 +1,442 @@
|
|||||||
@page "/sender"
|
@page "/sender"
|
||||||
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "Sender")]
|
||||||
|
|
||||||
<h3>EnvelopeSender</h3>
|
@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 {
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
@using DevExpress.Utils
|
@using DevExpress.Utils
|
||||||
@using DevExpress.XtraPrinting
|
@using DevExpress.XtraPrinting
|
||||||
@using DevExpress.XtraPrinting.Drawing
|
@using DevExpress.XtraPrinting.Drawing
|
||||||
|
@using EnvelopeGenerator.Application.Common.Dto
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
||||||
@@ -301,7 +302,10 @@ Shown="OnPopupShownAsync">
|
|||||||
bool IsLoggingOut;
|
bool IsLoggingOut;
|
||||||
|
|
||||||
IReadOnlyList<AnnotationDto> _annotations = [];
|
IReadOnlyList<AnnotationDto> _annotations = [];
|
||||||
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
|
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;
|
EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
||||||
SignatureCapture? _capturedSignature;
|
SignatureCapture? _capturedSignature;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ using EnvelopeGenerator.ReceiverUI.Options;
|
|||||||
using DevExpress.XtraReports.Services;
|
using DevExpress.XtraReports.Services;
|
||||||
using DevExpress.Blazor.Reporting;
|
using DevExpress.Blazor.Reporting;
|
||||||
using DevExpress.XtraReports.Web.Extensions;
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
builder.RootComponents.Add<App>("#app");
|
builder.RootComponents.Add<App>("#app");
|
||||||
@@ -20,9 +23,14 @@ builder.Services.AddScoped<DocumentService>();
|
|||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
builder.Services.AddScoped<AnnotationService>();
|
builder.Services.AddScoped<AnnotationService>();
|
||||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||||
builder.Services.AddScoped<SignatureService>();
|
builder.Services.AddScoped<DocReceiverElementService>();
|
||||||
builder.Services.AddScoped<SignatureCacheService>();
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeService>();
|
||||||
|
builder.Services.AddScoped<CultureService>();
|
||||||
|
|
||||||
|
// Localization services
|
||||||
|
builder.Services.AddLocalization();
|
||||||
|
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||||
@@ -41,5 +49,30 @@ builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
|||||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
||||||
|
// ⚠️ IMPORTANT: BLAZOR WASM-SPECIFIC CULTURE INITIALIZATION
|
||||||
|
// This approach sets DefaultThreadCurrentCulture globally, which is SAFE for WebAssembly
|
||||||
|
// because each user runs their own isolated app instance in their browser.
|
||||||
|
//
|
||||||
|
// ⚠️ TODO: REMOVE/REFACTOR WHEN MIGRATING TO BLAZOR SERVER/AUTO
|
||||||
|
// In Server/Auto render modes, this is DANGEROUS because:
|
||||||
|
// - Server runs a single shared instance for all users
|
||||||
|
// - Setting global culture affects ALL connected users simultaneously
|
||||||
|
// - Race conditions and culture conflicts will occur
|
||||||
|
//
|
||||||
|
// Migration Guide:
|
||||||
|
// - Option 1: Use RequestLocalizationMiddleware for per-request culture
|
||||||
|
// - Option 2: Use CascadingParameter with per-circuit culture state
|
||||||
|
// - See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
|
||||||
|
//
|
||||||
|
// Related files to update on migration:
|
||||||
|
// - LanguageSelector.razor (remove manual culture setting)
|
||||||
|
// - App.razor (may need CascadingValue for culture)
|
||||||
|
// - Startup/Program.cs (add middleware)
|
||||||
|
var cultureService = host.Services.GetRequiredService<CultureService>();
|
||||||
|
var culture = await cultureService.InitializeCultureAsync();
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||||
|
|
||||||
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
||||||
await host.RunAsync();
|
await host.RunAsync();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"EnvelopeGenerator.ReceiverUI": {
|
"EnvelopeGenerator.ReceiverUI": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.ReceiverUI.Models;
|
using EnvelopeGenerator.ReceiverUI.Models;
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
using EnvelopeGenerator.ReceiverUI.Options;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
||||||
|
/// Calls GET /api/auth/check/envelope/{envelopeKey}.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> CheckSenderAsync(CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check", cancel);
|
||||||
|
return response.StatusCode == HttpStatusCode.OK;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authenticates a sender user with username and password.
|
/// Authenticates a sender user with username and password.
|
||||||
/// Calls POST /api/auth?cookie=true with JSON body.
|
/// Calls POST /api/auth?cookie=true with JSON body.
|
||||||
@@ -78,4 +88,16 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
_ => SenderLoginResult.Error
|
_ => SenderLoginResult.Error
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs out the sender user by removing the authentication cookie.
|
||||||
|
/// Calls POST /api/auth/logout.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> LogoutSenderAsync(CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.PostAsync(
|
||||||
|
$"{_api.BaseUrl}/api/auth/logout",
|
||||||
|
null, cancel);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
EnvelopeGenerator.ReceiverUI/Services/CultureService.cs
Normal file
74
EnvelopeGenerator.ReceiverUI/Services/CultureService.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for managing application culture/localization.
|
||||||
|
/// </summary>
|
||||||
|
public class CultureService
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
private const string CULTURE_KEY = "AppCulture";
|
||||||
|
|
||||||
|
public CultureService(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of supported cultures.
|
||||||
|
/// </summary>
|
||||||
|
public static CultureInfo[] SupportedCultures { get; } = new[]
|
||||||
|
{
|
||||||
|
new CultureInfo("de-DE"),
|
||||||
|
new CultureInfo("en-US"),
|
||||||
|
new CultureInfo("fr-FR")
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the application culture and stores it in localStorage.
|
||||||
|
/// </summary>
|
||||||
|
public async Task SetCultureAsync(string culture)
|
||||||
|
{
|
||||||
|
if (!SupportedCultures.Any(c => c.Name == culture))
|
||||||
|
throw new ArgumentException($"Culture '{culture}' is not supported.", nameof(culture));
|
||||||
|
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CULTURE_KEY, culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the stored culture from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string?> GetCultureAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", CULTURE_KEY);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the culture from localStorage or browser settings.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<CultureInfo> InitializeCultureAsync()
|
||||||
|
{
|
||||||
|
var storedCulture = await GetCultureAsync();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(storedCulture) &&
|
||||||
|
SupportedCultures.Any(c => c.Name == storedCulture))
|
||||||
|
{
|
||||||
|
return new CultureInfo(storedCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to browser culture or default
|
||||||
|
var browserCulture = CultureInfo.CurrentCulture.Name;
|
||||||
|
var matchedCulture = SupportedCultures.FirstOrDefault(c => c.Name == browserCulture);
|
||||||
|
|
||||||
|
return matchedCulture ?? SupportedCultures[0]; // Default to German
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||||
|
|
||||||
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
public class DocReceiverElementService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
|
var url = $"{apiOptions.Value.BaseUrl}/api/DocReceiverElement/{Uri.EscapeDataString(envelopeKey)}";
|
||||||
var response = await http.GetAsync(url, cancel);
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
72
EnvelopeGenerator.ReceiverUI/Services/EnvelopeService.cs
Normal file
72
EnvelopeGenerator.ReceiverUI/Services/EnvelopeService.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.ReceiverUI.Models;
|
||||||
|
using EnvelopeGenerator.ReceiverUI.Options;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves <see cref="EnvelopeDto"/>s from the API.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly ApiOptions _apiOptions;
|
||||||
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public EnvelopeService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
_apiOptions = apiOptions.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches envelopes from the API with optional filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||||
|
public async Task<IEnumerable<EnvelopeDto>?> GetAsync(
|
||||||
|
int? id = null,
|
||||||
|
string? uuid = null,
|
||||||
|
bool? onlyActive = null,
|
||||||
|
bool? onlyCompleted = null,
|
||||||
|
CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var baseUrl = $"{_apiOptions.BaseUrl}/api/Envelope";
|
||||||
|
var queryParams = new Dictionary<string, string?>();
|
||||||
|
|
||||||
|
if (id.HasValue)
|
||||||
|
{
|
||||||
|
queryParams["Id"] = id.Value.ToString();
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(uuid))
|
||||||
|
{
|
||||||
|
queryParams["Uuid"] = uuid;
|
||||||
|
}
|
||||||
|
if (onlyActive.HasValue)
|
||||||
|
{
|
||||||
|
queryParams["OnlyActive"] = onlyActive.Value.ToString();
|
||||||
|
}
|
||||||
|
if (onlyCompleted.HasValue)
|
||||||
|
{
|
||||||
|
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
|
||||||
|
|
||||||
|
var response = await _http.GetAsync(url, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to load envelopes. Status: {statusCode} ({reasonPhrase})",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor
Normal file
80
EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
@using System.Globalization
|
||||||
|
@using EnvelopeGenerator.ReceiverUI.Services
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject CultureService CultureService
|
||||||
|
|
||||||
|
<div class="language-selector">
|
||||||
|
<button class="language-selector__trigger" @onclick="ToggleDropdown" aria-label="Select Language">
|
||||||
|
<span class="fi fi-@GetFlagCode(CurrentCulture)"></span>
|
||||||
|
<span class="language-selector__arrow">@GetLanguageName(CurrentCulture)</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (isOpen)
|
||||||
|
{
|
||||||
|
<div class="language-selector__dropdown">
|
||||||
|
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("de-DE"))">
|
||||||
|
<span class="fi fi-de"></span>
|
||||||
|
<span>Deutsch</span>
|
||||||
|
</button>
|
||||||
|
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("en-US"))">
|
||||||
|
<span class="fi fi-us"></span>
|
||||||
|
<span>English</span>
|
||||||
|
</button>
|
||||||
|
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("fr-FR"))">
|
||||||
|
<span class="fi fi-fr"></span>
|
||||||
|
<span>Français</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool isOpen = false;
|
||||||
|
private string CurrentCulture => CultureInfo.CurrentCulture.Name;
|
||||||
|
|
||||||
|
private void ToggleDropdown()
|
||||||
|
{
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ChangeLanguageAsync(string culture)
|
||||||
|
{
|
||||||
|
if (CultureInfo.CurrentCulture.Name != culture)
|
||||||
|
{
|
||||||
|
await CultureService.SetCultureAsync(culture);
|
||||||
|
|
||||||
|
// Set culture without page reload
|
||||||
|
var cultureInfo = new CultureInfo(culture);
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||||
|
|
||||||
|
// Navigate without reload to trigger re-render
|
||||||
|
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFlagCode(string culture)
|
||||||
|
{
|
||||||
|
return culture switch
|
||||||
|
{
|
||||||
|
"de-DE" => "de",
|
||||||
|
"en-US" => "us",
|
||||||
|
"fr-FR" => "fr",
|
||||||
|
_ => "de"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLanguageName(string culture)
|
||||||
|
{
|
||||||
|
return culture switch
|
||||||
|
{
|
||||||
|
"de-DE" => "Deutsch",
|
||||||
|
"en-US" => "English",
|
||||||
|
"fr-FR" => "Français",
|
||||||
|
_ => "Deutsch"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@
|
|||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<footer class="receiver-footer">
|
<footer class="receiver-footer">
|
||||||
|
<div class="receiver-footer__content">
|
||||||
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||||
<span class="receiver-footer__sep">|</span>
|
<span class="receiver-footer__sep">|</span>
|
||||||
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||||
|
</div>
|
||||||
|
<LanguageSelector />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,18 @@ article {
|
|||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receiver-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-footer__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -366,3 +366,74 @@ article {
|
|||||||
.receiver-footer__sep {
|
.receiver-footer__sep {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Language Selector (Footer) ──────────────────────────────────────────── */
|
||||||
|
.language-selector {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__trigger:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__arrow {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__dropdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 0.5rem);
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__option:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__option .fi {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector__option span:last-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|||||||
297
EnvelopeGenerator.ReceiverUI/wwwroot/css/sender-page.css
Normal file
297
EnvelopeGenerator.ReceiverUI/wwwroot/css/sender-page.css
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
.sender-dashboard-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-action-bar {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 3px solid rgba(126, 34, 206, 0.3);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-action-bar__inner {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-title-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-logo svg {
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3));
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1.125rem;
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
||||||
|
border-color: rgba(126, 34, 206, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||||
|
border-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--primary:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 16px rgba(126, 34, 206, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--danger {
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%);
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--danger:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--logout {
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-width: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-container {
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-container::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%);
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid rgba(126, 34, 206, 0.1);
|
||||||
|
padding: 0 2rem;
|
||||||
|
background: rgba(126, 34, 206, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab:hover {
|
||||||
|
color: #7e22ce;
|
||||||
|
background: rgba(126, 34, 206, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab--active {
|
||||||
|
color: #7e22ce;
|
||||||
|
border-bottom-color: #7e22ce;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-wrapper {
|
||||||
|
padding: 1.5rem 2rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide DevExpress empty cells */
|
||||||
|
.dxbl-grid-empty-cell {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--partly-signed,
|
||||||
|
.status-badge--completed {
|
||||||
|
background: rgba(129, 199, 132, 0.15);
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--queued,
|
||||||
|
.status-badge--sent {
|
||||||
|
background: rgba(255, 183, 77, 0.15);
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--deleted,
|
||||||
|
.status-badge--rejected,
|
||||||
|
.status-badge--withdrawn {
|
||||||
|
background: rgba(229, 115, 115, 0.15);
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--created,
|
||||||
|
.status-badge--saved {
|
||||||
|
background: rgba(100, 181, 246, 0.15);
|
||||||
|
color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--green {
|
||||||
|
background: #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--orange {
|
||||||
|
background: #ffb74d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--red {
|
||||||
|
background: #e57373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--blue {
|
||||||
|
background: #64b5f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #374151;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-badge--signed {
|
||||||
|
background: rgba(129, 199, 132, 0.15);
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-badge--unsigned {
|
||||||
|
background: rgba(229, 115, 115, 0.15);
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 768px) {
|
||||||
|
.sender-action-bar {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-action-bar__inner {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-content {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-wrapper {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tabs {
|
||||||
|
padding: 0 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab {
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
font-size: 0.813rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="EnvelopeGenerator.ReceiverUI.styles.css" rel="stylesheet" />
|
<link href="EnvelopeGenerator.ReceiverUI.styles.css" rel="stylesheet" />
|
||||||
<link href="css/app.css" rel="stylesheet" />
|
<link href="css/app.css" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" />
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.splash-screen {
|
.splash-screen {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -15,13 +15,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Inject] HttpClient Http { get; set; }
|
[Inject] IHttpClientFactory HttpClientFactory { get; set; } = default!;
|
||||||
|
|
||||||
List<string> RequiredFonts = new() {
|
List<string> RequiredFonts = new() {
|
||||||
"opensans.ttf"
|
"opensans.ttf"
|
||||||
};
|
};
|
||||||
|
|
||||||
protected async override Task OnInitializedAsync() {
|
protected async override Task OnInitializedAsync() {
|
||||||
await FontLoader.LoadFonts(Http, RequiredFonts);
|
await FontLoader.LoadFonts(HttpClientFactory, RequiredFonts);
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (LoginResult == EnvelopeLoginResult.NotFound) {
|
@if (LoginResult == EnvelopeLoginResult.NotFound)
|
||||||
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-start gap-2 py-2" role="alert">
|
<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">
|
<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" />
|
<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" />
|
||||||
@@ -35,7 +36,9 @@
|
|||||||
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else if (LoginResult == EnvelopeLoginResult.InvalidCode) {
|
}
|
||||||
|
else if (LoginResult == EnvelopeLoginResult.InvalidCode)
|
||||||
|
{
|
||||||
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
<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">
|
<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" />
|
<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" />
|
||||||
@@ -45,7 +48,9 @@
|
|||||||
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else if (LoginResult == EnvelopeLoginResult.Error) {
|
}
|
||||||
|
else if (LoginResult == EnvelopeLoginResult.Error)
|
||||||
|
{
|
||||||
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
<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">
|
<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="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" />
|
||||||
@@ -83,14 +88,17 @@
|
|||||||
style="border-left: none;"
|
style="border-left: none;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@onclick="() => ShowCode = !ShowCode">
|
@onclick="() => ShowCode = !ShowCode">
|
||||||
@if (ShowCode) {
|
@if (ShowCode)
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path 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="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="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 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" />
|
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z" />
|
||||||
</svg>
|
</svg>
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path 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="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" />
|
<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" />
|
||||||
@@ -104,10 +112,13 @@
|
|||||||
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||||
@onclick="SubmitAsync"
|
@onclick="SubmitAsync"
|
||||||
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
||||||
@if (IsLoading) {
|
@if (IsLoading)
|
||||||
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
<span>Überprüfen …</span>
|
<span>Überprüfen …</span>
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
||||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -132,12 +143,14 @@
|
|||||||
bool IsLoading;
|
bool IsLoading;
|
||||||
EnvelopeLoginResult? LoginResult;
|
EnvelopeLoginResult? LoginResult;
|
||||||
|
|
||||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
|
||||||
|
{
|
||||||
if (e.Key == "Enter")
|
if (e.Key == "Enter")
|
||||||
await SubmitAsync();
|
await SubmitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task SubmitAsync() {
|
async Task SubmitAsync()
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
||||||
|
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
@@ -146,7 +159,8 @@
|
|||||||
|
|
||||||
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success) {
|
if (result == EnvelopeLoginResult.Success)
|
||||||
|
{
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -156,3 +170,4 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ using DevExpress.XtraReports.Services;
|
|||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
// HTTP Client (uses Server's YARP proxy)
|
// Named HttpClient for API calls (both for services and DevExpress components)
|
||||||
builder.Services.AddScoped(sp => new HttpClient {
|
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default HttpClient (DevExpress PdfViewer requires this)
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient
|
||||||
|
{
|
||||||
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configuration Options
|
// Configuration Options
|
||||||
builder.Services.Configure<ApiOptions>(opts =>
|
|
||||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
|
||||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||||
|
|
||||||
@@ -51,5 +56,5 @@ builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
|||||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
await FontLoader.LoadFonts(host.Services.GetRequiredService<IHttpClientFactory>(), new List<string> { "opensans.ttf" });
|
||||||
await host.RunAsync();
|
await host.RunAsync();
|
||||||
|
|||||||
@@ -2,21 +2,15 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves annotation positions from the API.
|
/// Retrieves annotation positions from the API.
|
||||||
/// The URL is composed as <c>{BaseUrl}/api/Annotation/{envelopeKey}</c>.
|
/// Uses relative paths (/api/Annotation/{envelopeKey}).
|
||||||
/// During development, <c>BaseUrl</c> is empty so the request resolves to the
|
|
||||||
/// YARP-proxied route on the same origin, which currently serves
|
|
||||||
/// <c>fake-data/annotations.json</c>. To switch to real data, update the
|
|
||||||
/// YARP route in <c>yarp.json</c> — no code change required.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use SignatureService.")]
|
[Obsolete("Use SignatureService.")]
|
||||||
public class AnnotationService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class AnnotationService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
@@ -10,9 +8,8 @@ public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
|||||||
|
|
||||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||||
|
|
||||||
public class AuthService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class AuthService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
public class DocumentService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class DocumentService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the PDF bytes for the given envelope key from the API.
|
/// Fetches the PDF bytes for the given envelope key from the API.
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
||||||
/// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
public static class FontLoader {
|
public static class FontLoader
|
||||||
public async static Task LoadFonts(HttpClient httpClient, List<string> fontNames) {
|
{
|
||||||
foreach(var fontName in fontNames) {
|
public static async Task LoadFonts(IHttpClientFactory httpClientFactory, List<string> fontNames)
|
||||||
var fontBytes = await httpClient.GetByteArrayAsync($"fonts/{fontName}");
|
{
|
||||||
|
using var httpClient = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
|
||||||
|
foreach (var fontName in fontNames)
|
||||||
|
{
|
||||||
|
var fontBytes = await httpClient.GetByteArrayAsync($"/fonts/{fontName}");
|
||||||
DXFontRepository.Instance.AddFont(fontBytes);
|
DXFontRepository.Instance.AddFont(fontBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
@@ -9,9 +7,8 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client service for managing cached signatures via API.
|
/// Client service for managing cached signatures via API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SignatureCacheService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class SignatureCacheService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
|
||||||
|
|
||||||
public async Task SaveSignatureAsync(
|
public async Task SaveSignatureAsync(
|
||||||
string envelopeKey,
|
string envelopeKey,
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
public class SignatureService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class SignatureService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ try
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Load YARP configuration from yarp.json
|
||||||
|
builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||||
|
|
||||||
if (!builder.Environment.IsDevelopment())
|
if (!builder.Environment.IsDevelopment())
|
||||||
@@ -53,8 +56,25 @@ try
|
|||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
// Named HttpClient for internal API calls (same domain, uses relative paths)
|
// YARP Reverse Proxy (for forwarding auth requests to AuthHub)
|
||||||
builder.Services.AddHttpClient("EnvelopeGenerator.Server");
|
builder.Services.AddReverseProxy()
|
||||||
|
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||||
|
|
||||||
|
// HttpContextAccessor needed for SSR HttpClient configuration
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
// Named HttpClient for internal API calls
|
||||||
|
builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) =>
|
||||||
|
{
|
||||||
|
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
|
||||||
|
var request = httpContextAccessor.HttpContext?.Request;
|
||||||
|
|
||||||
|
if (request != null)
|
||||||
|
{
|
||||||
|
// Set base address to current host for SSR scenarios
|
||||||
|
client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// CORS Policy
|
// CORS Policy
|
||||||
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
||||||
@@ -290,9 +310,6 @@ try
|
|||||||
.AddEnvelopeGeneratorServices(config);
|
.AddEnvelopeGeneratorServices(config);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
// HttpClient for server-side components (e.g., MainLayout with FontLoader)
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
|
||||||
|
|
||||||
// Business Services (Server specific)
|
// Business Services (Server specific)
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
@@ -302,6 +319,9 @@ try
|
|||||||
builder.Services.AddScoped<SignatureCacheService>();
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
|
|
||||||
|
// SSR Authentication Service (for Envelope Receiver pages)
|
||||||
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService, EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
||||||
|
|
||||||
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
||||||
builder.Services.AddDevExpressBlazor();
|
builder.Services.AddDevExpressBlazor();
|
||||||
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
||||||
@@ -360,6 +380,9 @@ try
|
|||||||
// API Controllers (map before Blazor routing)
|
// API Controllers (map before Blazor routing)
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// YARP Reverse Proxy - forwards unmatched requests to configured backends
|
||||||
|
app.MapReverseProxy();
|
||||||
|
|
||||||
// Blazor routing
|
// Blazor routing
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-side authentication service for envelope receiver access validation.
|
||||||
|
/// Uses HttpContext to check JWT claims and envelope key authorization.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeAuthService : IEnvelopeAuthService
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly ILogger<EnvelopeAuthService> _logger;
|
||||||
|
|
||||||
|
public EnvelopeAuthService(
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
ILogger<EnvelopeAuthService> logger)
|
||||||
|
{
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsAuthenticated(string envelopeKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(envelopeKey))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("IsAuthenticated called with null or empty envelope key");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = _httpContextAccessor.HttpContext;
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("User is not authenticated for envelope {EnvelopeKey}", envelopeKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get envelope key from claims
|
||||||
|
var sub = GetEnvelopeKeyFromClaims(context.User);
|
||||||
|
|
||||||
|
// Verify envelope key matches
|
||||||
|
var isValid = sub == envelopeKey;
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Envelope key mismatch: Expected {ExpectedKey}, Got {ActualKey}",
|
||||||
|
envelopeKey,
|
||||||
|
sub ?? "(null)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("User authenticated for envelope {EnvelopeKey}", envelopeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string? GetAuthenticatedEnvelopeKey()
|
||||||
|
{
|
||||||
|
var context = _httpContextAccessor.HttpContext;
|
||||||
|
|
||||||
|
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return GetEnvelopeKeyFromClaims(context.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ClaimsPrincipal? GetCurrentUser()
|
||||||
|
{
|
||||||
|
return _httpContextAccessor.HttpContext?.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
// Try NameIdentifier first (standard claim)
|
||||||
|
var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
// Fallback to "sub" claim (JWT standard)
|
||||||
|
if (string.IsNullOrWhiteSpace(sub))
|
||||||
|
{
|
||||||
|
sub = user.FindFirst("sub")?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for handling envelope-specific authentication in SSR (Server-Side Rendering) context.
|
||||||
|
/// </summary>
|
||||||
|
public interface IEnvelopeAuthService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the current user is authenticated for the given envelope key.
|
||||||
|
/// Validates both that the user is authenticated AND that the envelope key matches their claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey">The envelope key to validate against user claims.</param>
|
||||||
|
/// <returns>True if user is authenticated and envelope key matches; otherwise false.</returns>
|
||||||
|
bool IsAuthenticated(string envelopeKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The envelope key if user is authenticated; otherwise null.</returns>
|
||||||
|
string? GetAuthenticatedEnvelopeKey();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current HttpContext user principal.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>ClaimsPrincipal if available; otherwise null.</returns>
|
||||||
|
ClaimsPrincipal? GetCurrentUser();
|
||||||
|
}
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"ReverseProxy": {
|
"ReverseProxy": {
|
||||||
"Routes": {
|
"Routes": {
|
||||||
"api-route": {
|
"auth-login": {
|
||||||
"ClusterId": "api-cluster",
|
"ClusterId": "auth-hub",
|
||||||
"Match": {
|
"Match": {
|
||||||
"Path": "/api/{**catch-all}"
|
"Path": "/api/auth",
|
||||||
}
|
"Methods": [ "POST" ]
|
||||||
},
|
},
|
||||||
"swagger-route": {
|
"Transforms": [
|
||||||
"ClusterId": "api-cluster",
|
{ "PathSet": "/api/auth/sign-flow" }
|
||||||
"Match": {
|
]
|
||||||
"Path": "/swagger/{**catch-all}"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"openapi-route": {
|
"auth-envelope-receiver-login": {
|
||||||
"ClusterId": "api-cluster",
|
"ClusterId": "auth-hub",
|
||||||
"Match": {
|
"Match": {
|
||||||
"Path": "/openapi/{**catch-all}"
|
"Path": "/api/Auth/envelope-receiver/{key}",
|
||||||
}
|
"Methods": [ "POST" ]
|
||||||
},
|
},
|
||||||
"scalar-route": {
|
"Transforms": [
|
||||||
"ClusterId": "api-cluster",
|
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
|
||||||
"Match": {
|
{
|
||||||
"Path": "/scalar/{**catch-all}"
|
"QueryValueParameter": "cookie",
|
||||||
|
"Set": "true"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Clusters": {
|
"Clusters": {
|
||||||
"api-cluster": {
|
"auth-hub": {
|
||||||
"Destinations": {
|
"Destinations": {
|
||||||
"api-destination": {
|
"primary": {
|
||||||
"Address": "https://localhost:8088"
|
"Address": "https://localhost:9090"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
|
|||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||||
|
OPEN_SSR_TASK.md = OPEN_SSR_TASK.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
||||||
|
|||||||
553
OPEN_SSR_TASK.md
Normal file
553
OPEN_SSR_TASK.md
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
# SSR Authentication Migration — Implementation Notes
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Migration from WASM client-side authentication to SSR (Server-Side Rendering) authentication for `EnvelopeReceiverPage.razor` to fix authentication issues in Blazor InteractiveServer mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
### Issue
|
||||||
|
`EnvelopeReceiverPage.razor` uses `@rendermode InteractiveServer` but was calling **WASM client service** `AuthService.CheckEnvelopeAccessAsync()`:
|
||||||
|
|
||||||
|
```razor
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
|
|
||||||
|
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Failed:**
|
||||||
|
- `AuthService` is a **WASM client service** that uses `IHttpClientFactory`
|
||||||
|
- In SSR context, `HttpContext` is required to configure the base address
|
||||||
|
- `CheckEnvelopeAccessAsync()` makes an HTTP request to `/api/auth/check/envelope/{key}`
|
||||||
|
- This request **goes to itself** (server calling its own endpoint), causing issues
|
||||||
|
- Returns `false` even when user is authenticated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
### Created New SSR Authentication Service
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
1. `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs` (Interface)
|
||||||
|
2. `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs` (Implementation)
|
||||||
|
|
||||||
|
**Purpose:** Direct `HttpContext.User` validation without HTTP requests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. IEnvelopeAuthService Interface
|
||||||
|
|
||||||
|
**Location:** `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
|
public interface IEnvelopeAuthService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the current user is authenticated for the given envelope key.
|
||||||
|
/// Validates both that the user is authenticated AND that the envelope key matches their claims.
|
||||||
|
/// </summary>
|
||||||
|
bool IsAuthenticated(string envelopeKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim).
|
||||||
|
/// </summary>
|
||||||
|
string? GetAuthenticatedEnvelopeKey();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current HttpContext user principal.
|
||||||
|
/// </summary>
|
||||||
|
ClaimsPrincipal? GetCurrentUser();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Methods:**
|
||||||
|
- `IsAuthenticated(string envelopeKey)`: Validates user auth + envelope key match
|
||||||
|
- `GetAuthenticatedEnvelopeKey()`: Extracts envelope key from claims
|
||||||
|
- `GetCurrentUser()`: Returns `ClaimsPrincipal` for advanced scenarios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. EnvelopeAuthService Implementation
|
||||||
|
|
||||||
|
**Location:** `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
|
- `IHttpContextAccessor`: Access current HTTP context
|
||||||
|
- `ILogger<EnvelopeAuthService>`: Structured logging
|
||||||
|
|
||||||
|
**Logic:**
|
||||||
|
```csharp
|
||||||
|
public bool IsAuthenticated(string envelopeKey)
|
||||||
|
{
|
||||||
|
// 1. Validate envelope key parameter
|
||||||
|
if (string.IsNullOrWhiteSpace(envelopeKey))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 2. Get HttpContext
|
||||||
|
var context = _httpContextAccessor.HttpContext;
|
||||||
|
|
||||||
|
// 3. Check if user is authenticated
|
||||||
|
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 4. Extract envelope key from claims
|
||||||
|
var sub = GetEnvelopeKeyFromClaims(context.User);
|
||||||
|
|
||||||
|
// 5. Verify match
|
||||||
|
return sub == envelopeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
// Try standard claim first
|
||||||
|
var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
// Fallback to JWT "sub" claim
|
||||||
|
if (string.IsNullOrWhiteSpace(sub))
|
||||||
|
sub = user.FindFirst("sub")?.Value;
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claim Priority:**
|
||||||
|
1. `ClaimTypes.NameIdentifier` (standard .NET claim)
|
||||||
|
2. `"sub"` (JWT standard claim)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Service Registration
|
||||||
|
|
||||||
|
**Location:** `EnvelopeGenerator.Server/Program.cs`
|
||||||
|
|
||||||
|
**Added:**
|
||||||
|
```csharp
|
||||||
|
// SSR Authentication Service (for Envelope Receiver pages)
|
||||||
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService,
|
||||||
|
EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lifetime:** `Scoped` (per-request, matches `IHttpContextAccessor`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. EnvelopeReceiverPage.razor Changes
|
||||||
|
|
||||||
|
**Changes Made (REVERTED - To Be Re-Applied):**
|
||||||
|
|
||||||
|
#### 4.1 Using Statements
|
||||||
|
```razor
|
||||||
|
@using EnvelopeGenerator.Server.Services
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 Dependency Injection
|
||||||
|
**Old:**
|
||||||
|
```razor
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
|
```
|
||||||
|
|
||||||
|
**New:**
|
||||||
|
```razor
|
||||||
|
@inject IEnvelopeAuthService EnvelopeAuth
|
||||||
|
@inject IHttpClientFactory HttpClientFactory
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3 Authentication Check in `OnInitializedAsync()`
|
||||||
|
**Old:**
|
||||||
|
```csharp
|
||||||
|
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||||
|
if (!hasAccess) {
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**New:**
|
||||||
|
```csharp
|
||||||
|
// ? SSR Authentication check via service
|
||||||
|
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ? Synchronous (no HTTP overhead)
|
||||||
|
- ? Direct `HttpContext.User` access
|
||||||
|
- ? No self-referencing HTTP calls
|
||||||
|
- ? Works in SSR context
|
||||||
|
|
||||||
|
#### 4.4 Logout Method
|
||||||
|
**Old:**
|
||||||
|
```csharp
|
||||||
|
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
**New:**
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ? SSR: Direct HTTP call instead of WASM client service
|
||||||
|
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Changed:**
|
||||||
|
- WASM `AuthService.LogoutEnvelopeReceiverAsync()` doesn't work in SSR
|
||||||
|
- Use named HttpClient `"EnvelopeGenerator.Server"` (configured in `Program.cs`)
|
||||||
|
- Graceful error handling (logout errors shouldn't block redirect)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining Tasks
|
||||||
|
|
||||||
|
### ? Completed
|
||||||
|
1. ? Created `IEnvelopeAuthService` interface
|
||||||
|
2. ? Implemented `EnvelopeAuthService` with `HttpContext` access
|
||||||
|
3. ? Registered service in `Program.cs`
|
||||||
|
4. ?? **REVERTED** `EnvelopeReceiverPage.razor` changes (merge conflict)
|
||||||
|
|
||||||
|
### ? TODO (Next Agent)
|
||||||
|
|
||||||
|
#### 1. Re-apply EnvelopeReceiverPage.razor Changes
|
||||||
|
**File:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Add using statement:
|
||||||
|
```razor
|
||||||
|
@using EnvelopeGenerator.Server.Services
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Replace injection:
|
||||||
|
```razor
|
||||||
|
@inject IEnvelopeAuthService EnvelopeAuth
|
||||||
|
@inject IHttpClientFactory HttpClientFactory
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
```razor
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update `OnInitializedAsync()` authentication check:
|
||||||
|
```csharp
|
||||||
|
// Replace this:
|
||||||
|
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||||
|
if (!hasAccess) {
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With this:
|
||||||
|
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Update `LogoutAsync()` method:
|
||||||
|
```csharp
|
||||||
|
async Task LogoutAsync() {
|
||||||
|
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return;
|
||||||
|
_isLoggingOut = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ? SSR: Direct HTTP call instead of WASM client service
|
||||||
|
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Test Authentication Flow
|
||||||
|
**Scenarios:**
|
||||||
|
- ? Valid cookie ? Page loads
|
||||||
|
- ? Invalid cookie ? Redirect to login
|
||||||
|
- ? No cookie ? Redirect to login
|
||||||
|
- ? Envelope key mismatch ? Redirect to login
|
||||||
|
- ? Logout ? Cookie cleared, redirect to login
|
||||||
|
|
||||||
|
#### 3. Remove WASM Client Services from SSR Pages
|
||||||
|
**Optional Cleanup:**
|
||||||
|
- Review other SSR pages (`EnvelopeReceiverPage_DxPdfViewer.razor`, etc.)
|
||||||
|
- Replace WASM client services with SSR equivalents where applicable
|
||||||
|
- Document which services are WASM-only vs SSR-compatible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication Flow Comparison
|
||||||
|
|
||||||
|
### ? Old Flow (WASM Client Service in SSR)
|
||||||
|
```
|
||||||
|
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
||||||
|
?
|
||||||
|
AuthService.CheckEnvelopeAccessAsync() (WASM client)
|
||||||
|
?
|
||||||
|
IHttpClientFactory.CreateClient("EnvelopeGenerator.Server")
|
||||||
|
?
|
||||||
|
GET /api/auth/check/envelope/{key}
|
||||||
|
?
|
||||||
|
[SELF-REFERENCING REQUEST - FAILS]
|
||||||
|
?
|
||||||
|
Returns false even when authenticated
|
||||||
|
```
|
||||||
|
|
||||||
|
### ? New Flow (SSR Service)
|
||||||
|
```
|
||||||
|
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
||||||
|
?
|
||||||
|
IEnvelopeAuthService.IsAuthenticated(envelopeKey)
|
||||||
|
?
|
||||||
|
IHttpContextAccessor.HttpContext.User (Direct access)
|
||||||
|
?
|
||||||
|
ClaimsPrincipal.FindFirst("sub" or NameIdentifier)
|
||||||
|
?
|
||||||
|
Compare with envelopeKey
|
||||||
|
?
|
||||||
|
Return true/false (synchronous, no HTTP)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Decisions
|
||||||
|
|
||||||
|
### Why Not Use `[Authorize]` Attribute?
|
||||||
|
- Blazor SSR components **don't support** `[Authorize]` at component level
|
||||||
|
- Would require `<AuthorizeView>` component (less clean)
|
||||||
|
- Custom service provides more control + logging
|
||||||
|
|
||||||
|
### Why Scoped Lifetime?
|
||||||
|
- `IHttpContextAccessor` is scoped (per-request)
|
||||||
|
- `EnvelopeAuthService` depends on `IHttpContextAccessor`
|
||||||
|
- Scoped ensures same `HttpContext` throughout request
|
||||||
|
|
||||||
|
### Why Two Claims (`NameIdentifier` + `"sub"`)?
|
||||||
|
- **`NameIdentifier`**: Standard .NET claim type
|
||||||
|
- **`"sub"`**: JWT standard claim
|
||||||
|
- Fallback ensures compatibility with different token formats
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging & Debugging
|
||||||
|
|
||||||
|
### Log Levels
|
||||||
|
- **Debug:** Successful authentication
|
||||||
|
- **Warning:** Null envelope key, key mismatch
|
||||||
|
- **Error:** (Reserved for future exceptions)
|
||||||
|
|
||||||
|
### Sample Logs
|
||||||
|
```
|
||||||
|
[Debug] User authenticated for envelope 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
||||||
|
[Warning] Envelope key mismatch: Expected abc123, Got 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
||||||
|
[Warning] IsAuthenticated called with null or empty envelope key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Unit Tests (TODO)
|
||||||
|
```csharp
|
||||||
|
// EnvelopeGenerator.Tests/Services/EnvelopeAuthServiceTests.cs
|
||||||
|
[Fact]
|
||||||
|
public void IsAuthenticated_ValidUser_ReturnsTrue() { ... }
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsAuthenticated_InvalidKey_ReturnsFalse() { ... }
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsAuthenticated_UnauthenticatedUser_ReturnsFalse() { ... }
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetAuthenticatedEnvelopeKey_ValidUser_ReturnsKey() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests (Manual)
|
||||||
|
1. ? Login with valid access code ? Cookie set
|
||||||
|
2. ? Navigate to `/envelope/{key}` ? Page loads
|
||||||
|
3. ? Logout ? Cookie cleared, redirect
|
||||||
|
4. ? Try accessing `/envelope/{key}` without cookie ? Redirect to login
|
||||||
|
5. ? Try accessing `/envelope/{wrongKey}` with valid cookie ? Redirect to login
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
- [x] Create `IEnvelopeAuthService` interface
|
||||||
|
- [x] Implement `EnvelopeAuthService`
|
||||||
|
- [x] Register service in `Program.cs`
|
||||||
|
- [ ] **Re-apply** `EnvelopeReceiverPage.razor` changes (after merge)
|
||||||
|
- [ ] Test authentication flow
|
||||||
|
- [ ] Add unit tests
|
||||||
|
- [ ] Update other SSR pages (if needed)
|
||||||
|
- [ ] Document in `COPILOT_CONTEXT.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Updates Needed
|
||||||
|
|
||||||
|
### COPILOT_CONTEXT.md
|
||||||
|
|
||||||
|
**Add Section:**
|
||||||
|
```markdown
|
||||||
|
## SSR Authentication Service
|
||||||
|
|
||||||
|
**Purpose:** Server-side authentication for Blazor InteractiveServer pages.
|
||||||
|
|
||||||
|
**Location:** `EnvelopeGenerator.Server/Services/`
|
||||||
|
|
||||||
|
**Service:** `IEnvelopeAuthService` / `EnvelopeAuthService`
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```razor
|
||||||
|
@inject IEnvelopeAuthService EnvelopeAuth
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Not Use WASM Client Services in SSR?**
|
||||||
|
- WASM client services use `IHttpClientFactory` with base address configuration
|
||||||
|
- SSR context requires `HttpContext` to configure base address
|
||||||
|
- Calling API endpoints from server-side component creates self-referencing requests
|
||||||
|
- Use `IEnvelopeAuthService` for direct `HttpContext.User` access instead
|
||||||
|
|
||||||
|
**Authentication Flow:**
|
||||||
|
1. JWT token stored in per-envelope cookie (`AuthTokenSignFLOWReceiver.{envelopeKey}`)
|
||||||
|
2. JWT middleware validates token, sets `HttpContext.User`
|
||||||
|
3. `EnvelopeAuthService` checks `ClaimsPrincipal.FindFirst("sub")` or `NameIdentifier`
|
||||||
|
4. Compares claim value with route parameter `{EnvelopeKey}`
|
||||||
|
|
||||||
|
**Claim Priority:**
|
||||||
|
1. `ClaimTypes.NameIdentifier` (standard .NET)
|
||||||
|
2. `"sub"` (JWT standard)
|
||||||
|
|
||||||
|
**Service Lifetime:** Scoped (per-request)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
|
### ? Don't Do This
|
||||||
|
```csharp
|
||||||
|
// SSR page using WASM client service
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
|
|
||||||
|
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Wrong:**
|
||||||
|
- Creates self-referencing HTTP request
|
||||||
|
- WASM client service doesn't work in SSR context
|
||||||
|
- Always returns `false` even when authenticated
|
||||||
|
|
||||||
|
### ? Do This Instead
|
||||||
|
```csharp
|
||||||
|
// SSR page using SSR authentication service
|
||||||
|
@inject IEnvelopeAuthService EnvelopeAuth
|
||||||
|
|
||||||
|
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Correct:**
|
||||||
|
- Direct `HttpContext.User` access
|
||||||
|
- Synchronous (no HTTP overhead)
|
||||||
|
- Works in SSR context
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### Related Files
|
||||||
|
- `EnvelopeGenerator.Server/Program.cs` (Service registration, JWT middleware)
|
||||||
|
- `EnvelopeGenerator.Server.Client/Services/AuthService.cs` (WASM client version)
|
||||||
|
- `EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` (WASM login page)
|
||||||
|
- `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` (SSR viewer page)
|
||||||
|
|
||||||
|
### JWT Configuration
|
||||||
|
**File:** `EnvelopeGenerator.Server/Program.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
||||||
|
{
|
||||||
|
opt.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
||||||
|
if (envelopeKey is not null)
|
||||||
|
{
|
||||||
|
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||||
|
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken))
|
||||||
|
context.Token = cookieToken;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
OnTokenValidated = context =>
|
||||||
|
{
|
||||||
|
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
||||||
|
var sub = context.Principal?.FindFirst("sub")?.Value;
|
||||||
|
|
||||||
|
if (envelopeKey is null || sub != envelopeKey)
|
||||||
|
context.Fail("Envelope key mismatch");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**What Was Done:**
|
||||||
|
1. Created SSR authentication service (`IEnvelopeAuthService` / `EnvelopeAuthService`)
|
||||||
|
2. Registered service in DI container
|
||||||
|
3. Updated `EnvelopeReceiverPage.razor` (REVERTED due to merge)
|
||||||
|
|
||||||
|
**What's Left:**
|
||||||
|
1. **Re-apply** `EnvelopeReceiverPage.razor` changes after merge
|
||||||
|
2. Test authentication flow
|
||||||
|
3. Add unit tests
|
||||||
|
4. Update documentation
|
||||||
|
|
||||||
|
**Key Insight:**
|
||||||
|
- **WASM client services ? SSR server services**
|
||||||
|
- Use `IHttpContextAccessor` for direct `HttpContext.User` access in SSR
|
||||||
|
- Avoid HTTP requests from server-side components to own endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2025-01-27
|
||||||
|
**Status:** ?? Partial (Service created, page changes reverted for merge)
|
||||||
|
**Next Agent:** Re-apply `EnvelopeReceiverPage.razor` changes + testing
|
||||||
Reference in New Issue
Block a user