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.
This commit is contained in:
@@ -3,20 +3,19 @@
|
|||||||
@using EnvelopeGenerator.Server.Client.Models
|
@using EnvelopeGenerator.Server.Client.Models
|
||||||
@using EnvelopeGenerator.Server.Client.Models.Constants
|
@using EnvelopeGenerator.Server.Client.Models.Constants
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
|
||||||
|
@using System.Security.Claims
|
||||||
@using Microsoft.Extensions.Options
|
@using Microsoft.Extensions.Options
|
||||||
@using EnvelopeGenerator.Server.Client.Options
|
@using EnvelopeGenerator.Server.Client.Options
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using DevExpress.Blazor
|
@using DevExpress.Blazor
|
||||||
@inject DocumentService DocumentService
|
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IOptions<ApiOptions> AppOptions
|
|
||||||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject DocReceiverElementService SignatureService
|
|
||||||
@inject SignatureCacheService SignatureCacheService
|
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
|
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
@inject ILogger<EnvelopeReceiverPage> logger
|
@inject ILogger<EnvelopeReceiverPage> logger
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
@@ -557,7 +556,8 @@
|
|||||||
bool _isLoggingOut = false;
|
bool _isLoggingOut = false;
|
||||||
DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef;
|
DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef;
|
||||||
IReadOnlyList<SignatureDto> _signatures = [];
|
IReadOnlyList<SignatureDto> _signatures = [];
|
||||||
EnvelopeReceiverDto? _envelopeReceiver;
|
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
|
ClaimsPrincipal? _receiverUser;
|
||||||
|
|
||||||
// Signature navigation state
|
// Signature navigation state
|
||||||
int _totalSignatures = 0;
|
int _totalSignatures = 0;
|
||||||
@@ -602,9 +602,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authentication
|
_receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
|
||||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
if (_receiverUser is null)
|
||||||
if (!hasAccess)
|
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
return;
|
return;
|
||||||
@@ -612,7 +611,7 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
|
var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser);
|
||||||
|
|
||||||
if (pdfBytes is { Length: > 0 })
|
if (pdfBytes is { Length: > 0 })
|
||||||
{
|
{
|
||||||
@@ -624,21 +623,20 @@
|
|||||||
_errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen.";
|
_errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen.";
|
||||||
}
|
}
|
||||||
|
|
||||||
var signatures = await SignatureService.GetAsync(EnvelopeKey);
|
_signatures = await PageDataService.GetSignaturesAsync(_receiverUser);
|
||||||
_signatures = signatures.Convert(UnitOfLength.Point);
|
|
||||||
|
|
||||||
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey);
|
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
|
||||||
if (_envelopeReceiver is null)
|
if (_envelopeReceiver is null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Envelope receiver data is null for envelope {EnvelopeKey}", EnvelopeKey);
|
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 to load cached signature first
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cachedSignature = await SignatureCacheService.GetSignatureAsync(EnvelopeKey);
|
var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser);
|
||||||
if (cachedSignature is not null)
|
if (cachedSignature is not null)
|
||||||
{
|
{
|
||||||
_capturedSignature = cachedSignature;
|
_capturedSignature = cachedSignature;
|
||||||
@@ -1094,7 +1092,10 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SignatureCacheService.SaveSignatureAsync(EnvelopeKey, _capturedSignature);
|
if (_receiverUser is not null)
|
||||||
|
{
|
||||||
|
await PageDataService.SaveCachedSignatureAsync(_receiverUser, _capturedSignature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -331,6 +331,8 @@ try
|
|||||||
|
|
||||||
// SSR Authentication Service (for Envelope Receiver pages)
|
// SSR Authentication Service (for Envelope Receiver pages)
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService, EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService, EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService>();
|
||||||
|
|
||||||
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
||||||
builder.Services.AddDevExpressBlazor();
|
builder.Services.AddDevExpressBlazor();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authorizes receiver access for interactive server pages without calling a controller endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeReceiverAuthorizationService(
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
IAuthorizationService authorizationService,
|
||||||
|
IOptions<AuthTokenKeys> authTokenKeyOptions,
|
||||||
|
IOptionsMonitor<JwtBearerOptions> jwtBearerOptionsMonitor,
|
||||||
|
ILogger<EnvelopeReceiverAuthorizationService> logger)
|
||||||
|
{
|
||||||
|
private readonly AuthTokenKeys _authTokenKeys = authTokenKeyOptions.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the authenticated receiver principal for the specified envelope key when authorization succeeds.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ClaimsPrincipal?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the current request is authorized for the specified envelope key.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> IsAuthorizedAsync(string envelopeKey, CancellationToken cancellationToken = default)
|
||||||
|
=> await AuthorizeAsync(envelopeKey, cancellationToken) is not null;
|
||||||
|
|
||||||
|
private async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads receiver page data directly from MediatR and distributed cache.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeReceiverPageDataService(
|
||||||
|
IMediator mediator,
|
||||||
|
IDistributedCache cache,
|
||||||
|
IOptions<CacheOptions> cacheOptions)
|
||||||
|
{
|
||||||
|
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the PDF document bytes for the authenticated receiver.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]?> GetDocumentAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken);
|
||||||
|
return document.ByteData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the current receiver's signature placeholders.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<SignatureDto>> 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<DocReceiverElementDto> elements)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return elements
|
||||||
|
.Where(element => element.ReceiverId == receiverId)
|
||||||
|
.Select(MapSignature)
|
||||||
|
.Convert(UnitOfLength.Point)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the envelope receiver data for the specified envelope key.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ApplicationEnvelopeReceiverDto?> GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new ReadEnvelopeReceiverQuery { Key = envelopeKey }, cancellationToken);
|
||||||
|
return result.SingleOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the cached signature for the authenticated receiver.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SignatureCaptureDto?> GetCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var json = await cache.GetStringAsync(GetSignatureCacheKey(user), cancellationToken);
|
||||||
|
return json is null ? null : JsonSerializer.Deserialize<SignatureCaptureDto>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the cached signature for the authenticated receiver.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the cached signature for the authenticated receiver.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user