From 489d2808a10622ec91c740948eed5b0766ed9669 Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 29 Jun 2026 01:26:43 +0200 Subject: [PATCH] Refactor EnvelopeReceiverPage for modular data handling Refactored `EnvelopeReceiverPage.razor` to use new services for receiver authentication and data retrieval. Introduced `EnvelopeReceiverAuthorizationService` for handling JWT-based authorization and `EnvelopeReceiverPageDataService` for centralized data access and caching. Updated dependency injection in `Program.cs` to register these services. Replaced direct service calls with `PageDataService` methods for document, signature, and receiver data retrieval. Improved logging with `ILogger` and added debug logs for token validation. Enhanced modularity, maintainability, and performance by consolidating logic and reducing coupling between components. --- .../Pages/EnvelopeReceiverPage.razor | 33 +++--- .../EnvelopeGenerator.Server/Program.cs | 2 + .../EnvelopeReceiverAuthorizationService.cs | 93 ++++++++++++++++ .../EnvelopeReceiverPageDataService.cs | 103 ++++++++++++++++++ 4 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor index 587f8b06..c13b1fda 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor @@ -3,20 +3,19 @@ @using EnvelopeGenerator.Server.Client.Models @using EnvelopeGenerator.Server.Client.Models.Constants @using EnvelopeGenerator.Server.Client.Services +@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver +@using System.Security.Claims @using Microsoft.Extensions.Options @using EnvelopeGenerator.Server.Client.Options @using Microsoft.JSInterop @using DevExpress.Blazor -@inject DocumentService DocumentService @inject NavigationManager Navigation -@inject IOptions AppOptions @inject IOptions PdfViewerOptions @inject IJSRuntime JSRuntime -@inject DocReceiverElementService SignatureService -@inject SignatureCacheService SignatureCacheService @inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService -@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService @inject AppVersionService AppVersion +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService @inject ILogger logger @implements IAsyncDisposable @@ -557,7 +556,8 @@ bool _isLoggingOut = false; DotNetObjectReference? _dotNetRef; IReadOnlyList _signatures = []; - EnvelopeReceiverDto? _envelopeReceiver; + EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; + ClaimsPrincipal? _receiverUser; // Signature navigation state int _totalSignatures = 0; @@ -602,9 +602,8 @@ return; } - // Check authentication - var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey); - if (!hasAccess) + _receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); + if (_receiverUser is null) { Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); return; @@ -612,7 +611,7 @@ try { - var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey); + var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); if (pdfBytes is { Length: > 0 }) { @@ -624,21 +623,20 @@ _errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen."; } - var signatures = await SignatureService.GetAsync(EnvelopeKey); - _signatures = signatures.Convert(UnitOfLength.Point); + _signatures = await PageDataService.GetSignaturesAsync(_receiverUser); - _envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey); + _envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); if (_envelopeReceiver is null) { logger.LogWarning("Envelope receiver data is null for envelope {EnvelopeKey}", EnvelopeKey); } - await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures); + logger.LogInformation("Loaded {SignatureCount} signatures for envelope {EnvelopeKey}", _signatures.Count, EnvelopeKey); // Try to load cached signature first try { - var cachedSignature = await SignatureCacheService.GetSignatureAsync(EnvelopeKey); + var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser); if (cachedSignature is not null) { _capturedSignature = cachedSignature; @@ -1094,7 +1092,10 @@ { try { - await SignatureCacheService.SaveSignatureAsync(EnvelopeKey, _capturedSignature); + if (_receiverUser is not null) + { + await PageDataService.SaveCachedSignatureAsync(_receiverUser, _capturedSignature); + } } catch { diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs index ce98e5b0..96ba37ca 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs @@ -331,6 +331,8 @@ try // SSR Authentication Service (for Envelope Receiver pages) builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // DevExpress Server-Side Services (CRITICAL for DxPdfViewer) builder.Services.AddDevExpressBlazor(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs new file mode 100644 index 00000000..181135c4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs @@ -0,0 +1,93 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using DigitalData.Auth.Claims; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Server.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Server.Services; + +/// +/// Authorizes receiver access for interactive server pages without calling a controller endpoint. +/// +public class EnvelopeReceiverAuthorizationService( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + IOptions authTokenKeyOptions, + IOptionsMonitor jwtBearerOptionsMonitor, + ILogger logger) +{ + private readonly AuthTokenKeys _authTokenKeys = authTokenKeyOptions.Value; + + /// + /// Returns the authenticated receiver principal for the specified envelope key when authorization succeeds. + /// + public async Task AuthorizeAsync(string envelopeKey, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(envelopeKey)) + return null; + + var httpContext = httpContextAccessor.HttpContext; + if (httpContext is null) + return null; + + if (await IsAuthorizedReceiverAsync(httpContext.User, envelopeKey, cancellationToken)) + return httpContext.User; + + var cookieName = CookieNames.GetEnvelopeReceiverCookieName(_authTokenKeys.Cookie, envelopeKey); + if (!httpContext.Request.Cookies.TryGetValue(cookieName, out var token) || string.IsNullOrWhiteSpace(token)) + { + logger.LogDebug("Receiver cookie '{CookieName}' was not found for envelope '{EnvelopeKey}'.", cookieName, envelopeKey); + return null; + } + + var principal = ValidateReceiverToken(token); + if (principal is null) + return null; + + if (!await IsAuthorizedReceiverAsync(principal, envelopeKey, cancellationToken)) + return null; + + httpContext.User = principal; + + return principal; + } + + /// + /// Checks whether the current request is authorized for the specified envelope key. + /// + public async Task IsAuthorizedAsync(string envelopeKey, CancellationToken cancellationToken = default) + => await AuthorizeAsync(envelopeKey, cancellationToken) is not null; + + private async Task IsAuthorizedReceiverAsync(ClaimsPrincipal? principal, string envelopeKey, CancellationToken cancellationToken) + { + if (principal?.Identity?.IsAuthenticated != true) + return false; + + var authorizationResult = await authorizationService.AuthorizeAsync(principal, AuthPolicy.Receiver); + if (!authorizationResult.Succeeded) + return false; + + var subject = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? principal.FindFirst("sub")?.Value; + + return string.Equals(subject, envelopeKey, StringComparison.Ordinal); + } + + private ClaimsPrincipal? ValidateReceiverToken(string token) + { + try + { + var tokenValidationParameters = jwtBearerOptionsMonitor.Get(AuthScheme.Receiver).TokenValidationParameters.Clone(); + var tokenHandler = new JwtSecurityTokenHandler(); + return tokenHandler.ValidateToken(token, tokenValidationParameters, out _); + } + catch (Exception ex) + { + logger.LogDebug(ex, "Receiver token validation failed."); + return null; + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs new file mode 100644 index 00000000..f2697a9e --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs @@ -0,0 +1,103 @@ +using System.Security.Claims; +using System.Text.Json; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; +using EnvelopeGenerator.Application.Documents.Queries; +using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; +using EnvelopeGenerator.Server.Client.Models; +using EnvelopeGenerator.Server.Client.Models.Constants; +using EnvelopeGenerator.Server.Extensions; +using EnvelopeGenerator.Server.Options; +using MediatR; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto; + +namespace EnvelopeGenerator.Server.Services; + +/// +/// Loads receiver page data directly from MediatR and distributed cache. +/// +public class EnvelopeReceiverPageDataService( + IMediator mediator, + IDistributedCache cache, + IOptions cacheOptions) +{ + private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:"; + + /// + /// Loads the PDF document bytes for the authenticated receiver. + /// + public async Task GetDocumentAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + { + var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken); + return document.ByteData; + } + + /// + /// Loads the current receiver's signature placeholders. + /// + public async Task> GetSignaturesAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + { + var receiverId = user.ReceiverId(); + var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken); + + if (document.Elements is not IEnumerable elements) + return []; + + return elements + .Where(element => element.ReceiverId == receiverId) + .Select(MapSignature) + .Convert(UnitOfLength.Point) + .ToList(); + } + + /// + /// Loads the envelope receiver data for the specified envelope key. + /// + public async Task GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancellationToken = default) + { + var result = await mediator.Send(new ReadEnvelopeReceiverQuery { Key = envelopeKey }, cancellationToken); + return result.SingleOrDefault(); + } + + /// + /// Loads the cached signature for the authenticated receiver. + /// + public async Task GetCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + { + var json = await cache.GetStringAsync(GetSignatureCacheKey(user), cancellationToken); + return json is null ? null : JsonSerializer.Deserialize(json); + } + + /// + /// Saves the cached signature for the authenticated receiver. + /// + public async Task SaveCachedSignatureAsync(ClaimsPrincipal user, SignatureCaptureDto signature, CancellationToken cancellationToken = default) + { + var json = JsonSerializer.Serialize(signature); + var options = cacheOptions.Value.SignatureCacheExpiration.HasValue + ? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value } + : new DistributedCacheEntryOptions(); + + await cache.SetStringAsync(GetSignatureCacheKey(user), json, options, cancellationToken); + } + + /// + /// Deletes the cached signature for the authenticated receiver. + /// + public Task DeleteCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + => cache.RemoveAsync(GetSignatureCacheKey(user), cancellationToken); + + private static string GetSignatureCacheKey(ClaimsPrincipal user) + => $"{SignatureCacheKeyPrefix}{user.ReceiverSignature()}"; + + private static SignatureDto MapSignature(DocReceiverElementDto element) => new() + { + Id = element.Id, + X = element.X, + Y = element.Y, + Page = element.Page, + SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType + }; +}