From c7bc9fbcca5d58ad01a9e8ffe2152fb87844bab1 Mon Sep 17 00:00:00 2001 From: TekH Date: Fri, 2 Oct 2026 01:25:43 +0200 Subject: [PATCH] Refactor EnvelopeType handling and improve validation Removed AutoMapper dependency in `ReadEnvelopeTypesQueryHandler` and replaced it with manual projection to address null-materialization issues. Updated `EnvelopeType` entity properties to nullable types to align with the database schema. Adjusted computed properties to handle null values gracefully. Enhanced `SenderEditorPage` with authentication checks, improved handling of reminder and expiry settings, and added validation for user input. Introduced `NormalizeSettings` and `ValidateSettings` methods to ensure consistent and valid configurations. Refactored checkbox value parsing for better reliability. Injected `AuthService` and `IStringLocalizer` for authentication and localization support. Improved error messaging and localized validation errors. Performed minor code cleanup and refactoring for better readability and maintainability. --- .../Queries/ReadEnvelopeTypesQuery.cs | 33 +++++-- .../Entities/EnvelopeType.cs | 28 +++--- .../Pages/SenderEditorPage.razor | 88 +++++++++++++++++-- 3 files changed, 121 insertions(+), 28 deletions(-) diff --git a/EnvelopeGenerator.Application/EnvelopeTypes/Queries/ReadEnvelopeTypesQuery.cs b/EnvelopeGenerator.Application/EnvelopeTypes/Queries/ReadEnvelopeTypesQuery.cs index 3d607e94..4196dae4 100644 --- a/EnvelopeGenerator.Application/EnvelopeTypes/Queries/ReadEnvelopeTypesQuery.cs +++ b/EnvelopeGenerator.Application/EnvelopeTypes/Queries/ReadEnvelopeTypesQuery.cs @@ -1,6 +1,5 @@ using EnvelopeGenerator.Application.Common.Dto; using MediatR; -using AutoMapper; using DigitalData.Core.Abstraction.Application.Repository; using EnvelopeGenerator.Domain.Entities; using Microsoft.EntityFrameworkCore; @@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest>; public class ReadEnvelopeTypesQueryHandler : IRequestHandler> { private readonly IRepository _repository; - private readonly IMapper _mapper; /// /// /// /// - /// - public ReadEnvelopeTypesQueryHandler(IRepository repository, IMapper mapper) + public ReadEnvelopeTypesQueryHandler(IRepository repository) { _repository = repository; - _mapper = mapper; } /// @@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler public async Task> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken) { - var types = await _repository.Query.ToListAsync(cancellationToken); - return _mapper.Map>(types); + // TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType. + // Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized. + return await _repository.Query + .AsNoTracking() + .Select(t => new EnvelopeTypeDto + { + Id = t.Id, + Title = t.Title ?? string.Empty, + Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language, + ExpiresDays = t.ExpiresDays, + CertificationType = (int?)t.CertificationType, + UseAccessCode = t.UseAccessCode, + FinalEmailToCreator = (int?)t.FinalEmailToCreator, + FinalEmailToReceivers = (int?)t.FinalEmailToReceivers, + AddedWhen = t.AddedWhen, + ChangedWhen = t.ChangedWhen, + ExpiresWarningDays = t.ExpiresWarningDays, + SendReminderEmails = t.SendReminderEmails, + FirstReminderDays = t.FirstReminderDays, + ReminderIntervalDays = t.ReminderIntervalDays, + ContractType = t.ContractType + }) + .ToListAsync(cancellationToken); } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Domain/Entities/EnvelopeType.cs b/EnvelopeGenerator.Domain/Entities/EnvelopeType.cs index 8ad0604b..58434917 100644 --- a/EnvelopeGenerator.Domain/Entities/EnvelopeType.cs +++ b/EnvelopeGenerator.Domain/Entities/EnvelopeType.cs @@ -22,41 +22,41 @@ namespace EnvelopeGenerator.Domain.Entities public string Language { get; set; } [Column("EXPIRES_DAYS")] - public int ExpiresDays { get; set; } + public int? ExpiresDays { get; set; } [Column("CERTIFICATION_TYPE")] - public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature; + public Constants.CertificationType? CertificationType { get; set; } [Column("USE_ACCESS_CODE")] - public bool UseAccessCode { get; set; } + public bool? UseAccessCode { get; set; } [Column("FINAL_EMAIL_TO_CREATOR")] - public Constants.FinalEmailType FinalEmailToCreator { get; set; } + public Constants.FinalEmailType? FinalEmailToCreator { get; set; } [Column("FINAL_EMAIL_TO_RECEIVERS")] - public Constants.FinalEmailType FinalEmailToReceivers { get; set; } + public Constants.FinalEmailType? FinalEmailToReceivers { get; set; } [Required] [Column("ADDED_WHEN", TypeName = "datetime")] public DateTime AddedWhen { get; set; } [Column("CHANGED_WHEN", TypeName = "datetime")] - public DateTime ChangedWhen { get; set; } + public DateTime? ChangedWhen { get; set; } [Column("EXPIRES_WARNING_DAYS")] - public int ExpiresWarningDays { get; set; } + public int? ExpiresWarningDays { get; set; } [Column("SEND_REMINDER_EMAILS")] - public bool SendReminderEmails { get; set; } = false; + public bool? SendReminderEmails { get; set; } [Column("FIRST_REMINDER_DAYS")] - public int FirstReminderDays { get; set; } = 0; + public int? FirstReminderDays { get; set; } [Column("REMINDER_INTERVAL_DAYS")] - public int ReminderIntervalDays { get; set; } = 0; + public int? ReminderIntervalDays { get; set; } [Column("CONTRACT_TYPE")] - public int ContractType { get; set; } + public int? ContractType { get; set; } [NotMapped] public bool TFA_Enabled { get; set; } = false; @@ -70,14 +70,14 @@ namespace EnvelopeGenerator.Domain.Entities [NotMapped] public int ExpiresWhenDays { - get => ExpiresDays; + get => ExpiresDays ?? 0; set => ExpiresDays = value; } [NotMapped] public int ExpiresWarningWhenDays { - get => ExpiresWarningDays; + get => ExpiresWarningDays ?? 0; set => ExpiresWarningDays = value; } @@ -86,4 +86,4 @@ namespace EnvelopeGenerator.Domain.Entities return Title; } } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor index 0ca6b7fd..871d3ea3 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor @@ -20,6 +20,7 @@ @inject ReceiverService ReceiverService @inject EnvelopeService EnvelopeService @inject EnvelopeTypeService EnvelopeTypeService +@inject AuthService AuthService @inject IStringLocalizer UiLocalizer @@ -696,6 +697,13 @@ protected override async Task OnInitializedAsync() { + var hasAccess = await AuthService.CheckSenderAccessAsync(); + if (!hasAccess) + { + NavigationManager.NavigateTo("/sender/login"); + return; + } + try { _envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList(); @@ -768,12 +776,14 @@ _finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No; _finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No; - _sendReminderEmails = envelope.EnvelopeType?.SendReminderEmails ?? false; - _firstReminderDays = envelope.EnvelopeType?.FirstReminderDays ?? 0; - _reminderIntervalDays = envelope.EnvelopeType?.ReminderIntervalDays ?? 0; + _sendReminderEmails = envelope.SendReminderEmails ?? envelope.EnvelopeType?.SendReminderEmails ?? false; + _firstReminderDays = envelope.FirstReminderDays ?? envelope.EnvelopeType?.FirstReminderDays ?? 0; + _reminderIntervalDays = envelope.ReminderIntervalDays ?? envelope.EnvelopeType?.ReminderIntervalDays ?? 0; _expiresWhenDays = envelope.ExpiresWhenDays ?? envelope.EnvelopeType?.ExpiresDays ?? 0; _expiresWarningWhenDays = envelope.ExpiresWarningWhenDays ?? envelope.EnvelopeType?.ExpiresWarningDays ?? 0; + NormalizeSettings(); + var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? []; _receivers = envelopeReceivers .Select((er, index) => new ReceiverDraft( @@ -962,7 +972,7 @@ void OnTfaChanged(ChangeEventArgs e) { - _tfaEnabled = e.Value as bool? ?? false; + _tfaEnabled = ParseCheckboxValue(e); if (_tfaEnabled) _useAccessCode = true; } @@ -975,12 +985,20 @@ return; } - _useAccessCode = e.Value as bool? ?? false; + _useAccessCode = ParseCheckboxValue(e); } void OnSendReminderEmailsChanged(ChangeEventArgs e) { - _sendReminderEmails = e.Value as bool? ?? false; + _sendReminderEmails = ParseCheckboxValue(e); + } + + static bool ParseCheckboxValue(ChangeEventArgs e) + { + if (e.Value is bool b) + return b; + + return bool.TryParse(e.Value?.ToString(), out var parsed) && parsed; } // ── PDF area click → place field ── @@ -1100,6 +1118,13 @@ async Task SaveDraftAsync() { + if (!ValidateSettings(out var settingsError)) + { + _saveErrorMessage = settingsError; + _savePopupVisible = true; + return; + } + _isSaving = true; _saveErrorMessage = null; await InvokeAsync(StateHasChanged); @@ -1140,6 +1165,13 @@ // ── Send envelope ── async Task SendAsync() { + if (!ValidateSettings(out var settingsError)) + { + _saveErrorMessage = settingsError; + _savePopupVisible = true; + return; + } + // ── Validation ── _titleTouched = true; if (!_pdfLoaded || _originalPdfBytes is null) @@ -1227,6 +1259,8 @@ CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send) { + NormalizeSettings(); + var envelopeId = _editingEnvelopeId ?? EnvelopeId ?? _loadedEnvelopeId @@ -1279,6 +1313,48 @@ }; } + void NormalizeSettings() + { + _language = string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language.Trim(); + + if (_tfaEnabled) + _useAccessCode = true; + + _firstReminderDays = Math.Max(0, _firstReminderDays); + _reminderIntervalDays = Math.Max(0, _reminderIntervalDays); + _expiresWhenDays = Math.Max(0, _expiresWhenDays); + _expiresWarningWhenDays = Math.Max(0, _expiresWarningWhenDays); + + if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays) + _expiresWarningWhenDays = _expiresWhenDays; + } + + bool ValidateSettings(out string? message) + { + NormalizeSettings(); + + if (_sendReminderEmails && _reminderIntervalDays <= 0) + { + message = L("SenderEditor_Validation_ReminderInterval", "Bitte geben Sie ein Erinnerungsintervall groesser als 0 Tage ein."); + return false; + } + + if (_sendReminderEmails && _firstReminderDays < 0) + { + message = L("SenderEditor_Validation_FirstReminder", "Bitte geben Sie gueltige Tage fuer die erste Erinnerung ein."); + return false; + } + + if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays) + { + message = L("SenderEditor_Validation_ExpiryWarning", "Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen."); + return false; + } + + message = null; + return true; + } + int? GetEnvelopeIdFromCurrentUri() { try