From 28d0b6f81c672efb131f92b74b43188e20175267 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 27 Oct 2026 11:37:25 +0100 Subject: [PATCH] Add feature to open receiver envelope URLs Introduce a new feature allowing users to open URLs for receiver envelopes directly from the sender page. This includes: - Injecting `EnvelopeReceiverService` in `SenderPage.razor`. - Adding a UI button to open receiver envelope URLs. - Implementing `OpenReceiverEnvelopeUrlAsync` for URL handling. - Extending `EnvelopeReceiverService` with methods for URL retrieval and feature check. - Updating `EnvelopeReceiverController` with new endpoints for URL access. - Introducing `SenderFeatureAccessOptions` for managing feature access. - Configuring feature access in `Program.cs` and `appsettings.json`. - Restricting feature usage to specified email addresses. --- .../Pages/SenderPage.razor | 93 ++++++++++++++----- .../Services/EnvelopeReceiverService.cs | 30 ++++++ .../Controllers/EnvelopeReceiverController.cs | 39 +++++++- .../Options/SenderFeatureAccessOptions.cs | 8 ++ .../EnvelopeGenerator.Server/Program.cs | 1 + .../EnvelopeGenerator.Server/appsettings.json | 3 + 6 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/SenderFeatureAccessOptions.cs diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor index dcd67edf..7882b482 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor @@ -20,6 +20,7 @@ @inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService @inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService @inject EnvelopeGenerator.Server.Client.Services.PdfSignatureService PdfSignatureService +@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService @inject EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService @inject ILogger Logger @inject NavigationManager Navigation @@ -315,12 +316,13 @@ @foreach (var receiver in envelope.EnvelopeReceivers) {
- @{ - var isRejected = IsReceiverRejected(envelope, receiver); - var isSigned = IsReceiverSigned(envelope, receiver); - var accessCode = receiver.AccessCode; - var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode); - } + @{ + var isRejected = IsReceiverRejected(envelope, receiver); + var isSigned = IsReceiverSigned(envelope, receiver); + var accessCode = receiver.AccessCode; + var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode); + var hasReceiverEnvelopeUrl = CanOpenReceiverEnvelopeUrl(envelope, receiver); + } @if (isRejected) { @@ -349,27 +351,40 @@ @receiver.Name @receiver.Receiver?.EmailAddress
-
- @L("AccessCode", "Access code") -
- + @L("AccessCode", "Access code") +
+ - -
-
-
- } - + + + + @if (_canUseReceiverEnvelopeUrl) + { + + } + + + + } + } else { @@ -544,6 +559,7 @@ private string _activeTab = "active"; private bool _isLoading = true; private bool _isLoggingOut = false; + private bool _canUseReceiverEnvelopeUrl = false; private string? _errorMessage; private DxGrid? _grid; @@ -603,6 +619,8 @@ try { await ThemeService.LoadFromServerAsync(); } catch { /* swallow — default theme remains active */ } + _canUseReceiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlFeatureEnabledAsync(); + // Load logo config — authenticated, so this should succeed var logos = await CustomImagesService.GetAsync(); _appLogo = logos.App; @@ -987,6 +1005,35 @@ } } + async Task OpenReceiverEnvelopeUrlAsync(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver) + { + if (!_canUseReceiverEnvelopeUrl) + return; + + if (!CanOpenReceiverEnvelopeUrl(envelope, receiver)) + return; + + try + { + var receiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlAsync( + envelope.Uuid, + receiver.Receiver!.Signature!); + + if (string.IsNullOrWhiteSpace(receiverEnvelopeUrl)) + return; + + await JSRuntime.InvokeVoidAsync("open", receiverEnvelopeUrl, "_blank", "noopener,noreferrer"); + } + catch + { + await JSRuntime.InvokeVoidAsync("console.warn", "[Sender] Failed to open receiver envelope URL."); + } + } + + static bool CanOpenReceiverEnvelopeUrl(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver) + => !string.IsNullOrWhiteSpace(envelope.Uuid) + && !string.IsNullOrWhiteSpace(receiver.Receiver?.Signature); + void OnCustomizeElement(GridCustomizeElementEventArgs e) { // Future: Add custom row coloring based on status if needed diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeReceiverService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeReceiverService.cs index 656cdbac..5ab1d1fb 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeReceiverService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeReceiverService.cs @@ -15,6 +15,8 @@ namespace EnvelopeGenerator.Server.Client.Services; public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory) { private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + private sealed record ReceiverEnvelopeUrlResponse(string Url); + private sealed record ReceiverEnvelopeUrlEnabledResponse(bool Enabled); /// /// Fetches the envelope receiver data for the given envelope key from the API. @@ -63,4 +65,32 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory) return await response.Content.ReadFromJsonAsync(_jsonOptions, cancel); } + + public async Task GetReceiverEnvelopeUrlAsync(string envelopeUuid, string receiverSignature, CancellationToken cancel = default) + { + if (string.IsNullOrWhiteSpace(envelopeUuid) || string.IsNullOrWhiteSpace(receiverSignature)) + return null; + + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var url = $"/api/EnvelopeReceiver/receiver-envelope-url?envelopeUuid={Uri.EscapeDataString(envelopeUuid)}&receiverSignature={Uri.EscapeDataString(receiverSignature)}"; + var response = await http.GetAsync(url, cancel); + + if (!response.IsSuccessStatusCode) + return null; + + var payload = await response.Content.ReadFromJsonAsync(_jsonOptions, cancel); + return payload?.Url; + } + + public async Task GetReceiverEnvelopeUrlFeatureEnabledAsync(CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await http.GetAsync("/api/EnvelopeReceiver/receiver-envelope-url/enabled", cancel); + + if (!response.IsSuccessStatusCode) + return false; + + var payload = await response.Content.ReadFromJsonAsync(_jsonOptions, cancel); + return payload?.Enabled == true; + } } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs index 97f42db9..fe85f9ed 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs @@ -15,6 +15,8 @@ using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Server.Extensions; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver; +using EnvelopeGenerator.Application.Common.Extensions; +using EnvelopeGenerator.Server.Options; namespace EnvelopeGenerator.Server.Controllers; @@ -36,11 +38,12 @@ public class EnvelopeReceiverController : ControllerBase private readonly IEnvelopeReceiverExecutor _erExecutor; private readonly IDocumentExecutor _documentExecutor; private readonly string _cnnStr; + private readonly IOptionsMonitor _senderFeatureAccessOptions; /// /// Konstruktor für den EnvelopeReceiverController. /// - public EnvelopeReceiverController(ILogger logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions csOpt) + public EnvelopeReceiverController(ILogger logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions csOpt, IOptionsMonitor senderFeatureAccessOptions) { _logger = logger; _mediator = mediator; @@ -49,6 +52,7 @@ public class EnvelopeReceiverController : ControllerBase _erExecutor = erExecutor; _documentExecutor = documentExecutor; _cnnStr = csOpt.Value.Value; + _senderFeatureAccessOptions = senderFeatureAccessOptions; } /// @@ -112,6 +116,39 @@ public class EnvelopeReceiverController : ControllerBase return name is null ? NotFound() : Ok(name); } + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpGet("receiver-envelope-url")] + public IActionResult GetReceiverEnvelopeUrl([FromQuery] string envelopeUuid, [FromQuery] string receiverSignature) + { + if (!CanUseReceiverEnvelopeUrlFeature()) + return Forbid(); + + if (string.IsNullOrWhiteSpace(envelopeUuid) || string.IsNullOrWhiteSpace(receiverSignature)) + return BadRequest("Envelope uuid and receiver signature are required."); + + var envelopeKey = (envelopeUuid, receiverSignature).ToEnvelopeKey(); + var absoluteUrl = $"{Request.Scheme}://{Request.Host}/envelope/{Uri.EscapeDataString(envelopeKey)}"; + + return Ok(new { url = absoluteUrl }); + } + + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpGet("receiver-envelope-url/enabled")] + public IActionResult GetReceiverEnvelopeUrlFeatureEnabled() + { + return Ok(new { enabled = CanUseReceiverEnvelopeUrlFeature() }); + } + + private bool CanUseReceiverEnvelopeUrlFeature() + { + var senderEmail = User.GetEmail(); + if (string.IsNullOrWhiteSpace(senderEmail)) + return false; + + var allowedEmails = _senderFeatureAccessOptions.CurrentValue.AllowedReceiverEnvelopeUrlEmails; + return allowedEmails.Any(email => string.Equals(email?.Trim(), senderEmail, StringComparison.OrdinalIgnoreCase)); + } + /// /// Returns recent receiver suggestions for the authenticated sender. /// diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/SenderFeatureAccessOptions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/SenderFeatureAccessOptions.cs new file mode 100644 index 00000000..67b1a342 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/SenderFeatureAccessOptions.cs @@ -0,0 +1,8 @@ +namespace EnvelopeGenerator.Server.Options; + +public sealed class SenderFeatureAccessOptions +{ + public const string SectionName = "SenderFeatureAccess"; + + public List AllowedReceiverEnvelopeUrlEmails { get; set; } = []; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs index 2a717d1c..577136b1 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs @@ -298,6 +298,7 @@ try // Cache options builder.Services.Configure(config.GetSection(CacheOptions.SectionName)); + builder.Services.Configure(config.GetSection(SenderFeatureAccessOptions.SectionName)); // Distributed Cache - SQL Server builder.Services.AddDistributedSqlServerCache(options => diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json index 09a95e4c..be976dc6 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json @@ -212,6 +212,9 @@ "TableName": "TBDD_CACHE" } }, + "SenderFeatureAccess": { + "AllowedReceiverEnvelopeUrlEmails": ["h.tek@digitaldata.works"] + }, "MainPageTitle": null, "AnnotationParams": { "Background": {