Compare commits
2 Commits
42a853cb0d
...
feat/serve
| Author | SHA1 | Date | |
|---|---|---|---|
| 380a5f4c0c | |||
| 0f53d0509e |
@@ -49,7 +49,7 @@ public sealed record Signature
|
||||
public string? Position
|
||||
{
|
||||
get => _position;
|
||||
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
||||
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,4 +62,4 @@ public sealed record Signature
|
||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||
/// </summary>
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||
|
||||
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||
{
|
||||
public required Envelope Envelope { get; init; }
|
||||
|
||||
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||
|
||||
public required string Reason { get; init; }
|
||||
|
||||
public required string SenderReference { get; init; }
|
||||
|
||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||
|
||||
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||
|
||||
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||
{
|
||||
public SendDeletedMailHandler(
|
||||
IRepository<EmailTemplate> tempRepo,
|
||||
IRepository<EmailOut> emailOutRepo,
|
||||
IOptions<MailParams> mailParamsOptions,
|
||||
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||
{
|
||||
emailOut.ReferenceString = notification.EmailAddress;
|
||||
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||
}
|
||||
|
||||
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||
{
|
||||
var senderName = notification.Envelope.User?.Email;
|
||||
if (string.IsNullOrWhiteSpace(senderName))
|
||||
senderName = notification.SenderReference;
|
||||
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||
["[REASON]"] = notification.Reason
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -65,18 +65,21 @@ public static class DependencyInjection
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||
|
||||
// 1. AnnotationBehavior - Saves annotations (executes second)
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
|
||||
|
||||
// 2. AnnotationBehavior - Saves annotations for legacy web flow
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||
|
||||
// 2. DocStatusBehavior - Creates document status (executes third)
|
||||
// 3. DocStatusBehavior - Creates document status
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||
|
||||
// 3. HistoryBehavior - Records history (executes fourth)
|
||||
// 4. HistoryBehavior - Records history
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||
|
||||
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
||||
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
|
||||
// If EnvelopeReceiver is not provided, query it from database
|
||||
if (request.EnvelopeReceiver is null)
|
||||
{
|
||||
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
||||
var er = await _erRepo.Query
|
||||
.Where(request, notnull: true)
|
||||
.Include(x => x.Envelope)
|
||||
.Include(x => x.Receiver)
|
||||
.SingleOrDefaultAsync(cancellationToken)
|
||||
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||
|
||||
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
||||
|
||||
@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||
|
||||
var elements = await _elementRepo
|
||||
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
||||
.Where(e => e.ReceiverId == request.Receiver.Id)
|
||||
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
||||
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
foreach (var element in elements)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
|
||||
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
|
||||
|
||||
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
|
||||
{
|
||||
[Required]
|
||||
public int EnvelopeId { get; init; }
|
||||
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
internal int UserId { get; private set; }
|
||||
|
||||
public DeleteEnvelopeCommand WithAuth(int userId)
|
||||
{
|
||||
UserId = userId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Histories.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
|
||||
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
||||
{
|
||||
private readonly IRepository<Envelope> _envelopeRepository;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public DeleteEnvelopeCommandHandler(
|
||||
IRepository<Envelope> envelopeRepository,
|
||||
IMediator mediator)
|
||||
{
|
||||
_envelopeRepository = envelopeRepository;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
||||
{
|
||||
var reason = request.Reason?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
throw new BadRequestException("Please provide a reason.");
|
||||
|
||||
var envelope = await _envelopeRepository.Query
|
||||
.Include(e => e.User)
|
||||
.Include(e => e.EnvelopeReceivers)
|
||||
.ThenInclude(er => er.Receiver)
|
||||
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
||||
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
||||
|
||||
if (envelope.UserId != request.UserId)
|
||||
throw new ForbiddenException();
|
||||
|
||||
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
||||
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
||||
|
||||
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
||||
? EnvelopeStatus.EnvelopeWithdrawn
|
||||
: EnvelopeStatus.EnvelopeDeleted;
|
||||
|
||||
envelope.Status = targetStatus;
|
||||
envelope.Comment = reason;
|
||||
envelope.ChangedWhen = DateTime.Now;
|
||||
|
||||
await _envelopeRepository.UpdateAsync(envelope, e => e.Id == envelope.Id, cancel);
|
||||
|
||||
var userReference = envelope.User?.Email;
|
||||
if (string.IsNullOrWhiteSpace(userReference))
|
||||
throw new BadRequestException("Envelope sender email is missing.");
|
||||
|
||||
await _mediator.Send(new CreateHistoryCommand
|
||||
{
|
||||
EnvelopeId = envelope.Id,
|
||||
UserReference = userReference,
|
||||
Status = targetStatus,
|
||||
Comment = reason
|
||||
}, cancel);
|
||||
|
||||
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||
{
|
||||
if (envelopeReceiver.Receiver is null)
|
||||
continue;
|
||||
|
||||
await _mediator.Publish(new EnvelopeDeletedNotification
|
||||
{
|
||||
Envelope = envelope,
|
||||
EnvelopeReceiver = envelopeReceiver,
|
||||
Reason = reason,
|
||||
SenderReference = userReference
|
||||
}, cancel);
|
||||
}
|
||||
|
||||
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
||||
}
|
||||
}
|
||||
@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
#region Trigger
|
||||
// Configure entities to handle database triggers
|
||||
void AddTrigger<T>() where T : class => _triggers
|
||||
.Where(t => t.Key == typeof(T).Name)
|
||||
.SelectMany(t => t.Value)
|
||||
.ForEach(tName =>
|
||||
void AddTrigger<T>() where T : class
|
||||
{
|
||||
var entityName = typeof(T).Name;
|
||||
|
||||
var configuredTriggerNames = _triggers
|
||||
.Where(t => t.Key == entityName)
|
||||
.SelectMany(t => t.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var triggerName in configuredTriggerNames)
|
||||
{
|
||||
#if NET
|
||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(tName));
|
||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(triggerName));
|
||||
#endif
|
||||
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", tName, typeof(T).Name);
|
||||
});
|
||||
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", triggerName, entityName);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: call add trigger methods with attributes and reflection
|
||||
AddTrigger<Config>();
|
||||
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
#if NETFRAMEWORK
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
public sealed record SignatureSubmitDto
|
||||
{
|
||||
public required int Id { get; init; }
|
||||
public required string DataUrl { get; init; }
|
||||
public required string FullName { get; init; }
|
||||
public string? Position { get; init; }
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
@@ -12,8 +12,6 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
||||
public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private sealed record DeleteEnvelopeRequest(string Reason);
|
||||
private sealed record ErrorResponse(string? Message);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches envelopes from the API with optional filters.
|
||||
@@ -63,35 +61,4 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int envelopeId, string reason, CancellationToken cancel = default)
|
||||
{
|
||||
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/Envelope/{envelopeId}")
|
||||
{
|
||||
Content = JsonContent.Create(new DeleteEnvelopeRequest(reason), options: _jsonOptions)
|
||||
};
|
||||
|
||||
var response = await httpClient.SendAsync(request, cancel);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
string? message = null;
|
||||
try
|
||||
{
|
||||
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>(_jsonOptions, cancel);
|
||||
message = error?.Message;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message = null;
|
||||
}
|
||||
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||
throw new HttpRequestException(
|
||||
$"Failed to delete envelope. Status: {statusCode} ({reasonPhrase}){(string.IsNullOrWhiteSpace(message) ? string.Empty : $" - {message}")}",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
@@ -21,4 +22,32 @@ public class SignatureService(IHttpClientFactory httpClientFactory)
|
||||
var result = await response.Content.ReadFromJsonAsync<List<SignatureDto>>(_jsonOptions, cancel);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
public async Task SubmitAsync(string envelopeKey, IEnumerable<SignatureSubmitDto> signatures, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
|
||||
var payload = signatures
|
||||
.Select(signature => new Signature
|
||||
{
|
||||
Id = signature.Id,
|
||||
DataUrl = signature.DataUrl,
|
||||
FullName = signature.FullName,
|
||||
Position = signature.Position,
|
||||
Place = signature.Place
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var response = await http.PostAsJsonAsync(
|
||||
$"/api/Signature/submit/{Uri.EscapeDataString(envelopeKey)}",
|
||||
payload,
|
||||
cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancel);
|
||||
throw new HttpRequestException(
|
||||
$"Failed to submit signature for envelope {envelopeKey}: {(int)response.StatusCode} {response.StatusCode} - {body}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,54 +521,6 @@
|
||||
</BodyContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
<DxPopup @bind-Visible="@_deletePopupVisible"
|
||||
HeaderText="Umschlag löschen"
|
||||
Width="560px"
|
||||
MaxWidth="95vw"
|
||||
ShowFooter="true"
|
||||
CloseOnOutsideClick="false"
|
||||
ShowCloseButton="true"
|
||||
CloseOnEscape="true"
|
||||
ShowHeader="true"
|
||||
CssClass="sender-delete-popup">
|
||||
<BodyContentTemplate>
|
||||
<div style="display:flex;flex-direction:column;gap:0.75rem;">
|
||||
<p style="margin:0;color:#374151;font-size:0.9rem;line-height:1.45;">
|
||||
Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.
|
||||
</p>
|
||||
@if (_selectedEnvelope is not null)
|
||||
{
|
||||
<p style="margin:0;color:#6b7280;font-size:0.825rem;">
|
||||
Umschlag: <strong>#@_selectedEnvelope.Id - @_selectedEnvelope.Title</strong>
|
||||
</p>
|
||||
}
|
||||
<DxMemo @bind-Text="_deleteReason"
|
||||
NullText="Begründung eingeben..."
|
||||
Rows="5"
|
||||
MaxLength="2000"
|
||||
CssClass="w-100" />
|
||||
@if (!string.IsNullOrWhiteSpace(_deleteErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" style="margin:0;padding:0.5rem 0.75rem;font-size:0.85rem;">
|
||||
@_deleteErrorMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.5rem;width:100%;">
|
||||
<DxButton Text="Abbrechen"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click="CloseDeletePopup"
|
||||
Enabled="@(!_isDeletingEnvelope)" />
|
||||
<DxButton Text="Löschen"
|
||||
RenderStyle="ButtonRenderStyle.Danger"
|
||||
Click="ConfirmDeleteEnvelopeAsync"
|
||||
Enabled="@(!_isDeletingEnvelope)" />
|
||||
</div>
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@code {
|
||||
private IEnumerable<EnvelopeDto>? _allEnvelopes;
|
||||
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
||||
@@ -597,11 +549,6 @@
|
||||
private bool _exportSelectedRowsOnly = false;
|
||||
private bool _isExporting = false;
|
||||
|
||||
private bool _deletePopupVisible = false;
|
||||
private bool _isDeletingEnvelope = false;
|
||||
private string _deleteReason = string.Empty;
|
||||
private string? _deleteErrorMessage;
|
||||
|
||||
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
||||
private const string GridKeyActive = "sender.active-envelopes";
|
||||
private const string GridKeyCompleted = "sender.completed-envelopes";
|
||||
@@ -734,51 +681,7 @@
|
||||
void DeleteEnvelope()
|
||||
{
|
||||
if (_selectedEnvelope == null) return;
|
||||
_deleteReason = string.Empty;
|
||||
_deleteErrorMessage = null;
|
||||
_deletePopupVisible = true;
|
||||
}
|
||||
|
||||
void CloseDeletePopup()
|
||||
{
|
||||
if (_isDeletingEnvelope)
|
||||
return;
|
||||
|
||||
_deletePopupVisible = false;
|
||||
_deleteErrorMessage = null;
|
||||
}
|
||||
|
||||
async Task ConfirmDeleteEnvelopeAsync()
|
||||
{
|
||||
if (_selectedEnvelope is null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_deleteReason))
|
||||
{
|
||||
_deleteErrorMessage = "Bitte geben Sie einen Löschgrund an.";
|
||||
return;
|
||||
}
|
||||
|
||||
_isDeletingEnvelope = true;
|
||||
_deleteErrorMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
await EnvelopeService.DeleteAsync(_selectedEnvelope.Id, _deleteReason.Trim());
|
||||
_deletePopupVisible = false;
|
||||
_selectedEnvelope = null;
|
||||
await LoadEnvelopesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_deleteErrorMessage = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDeletingEnvelope = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
// TODO: Show delete confirmation dialog
|
||||
}
|
||||
|
||||
async Task LogoutAsync()
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||
@inject SignatureService SignatureService
|
||||
@inject AppVersionService AppVersion
|
||||
@inject IMemoryCache MemoryCache
|
||||
@inject ILogger<ReceiverSignedPage> Logger
|
||||
@@ -122,6 +123,12 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_submitErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger mb-0" role="alert" style="font-size: 0.85rem;">
|
||||
@_submitErrorMessage
|
||||
</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
||||
@@ -161,17 +168,48 @@
|
||||
// ----- Submit / logout state -----
|
||||
bool _isLoggingOut = false;
|
||||
bool _submitConfirmVisible = false;
|
||||
string? _submitErrorMessage;
|
||||
|
||||
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
|
||||
|
||||
async Task SubmitAndLogoutAsync()
|
||||
{
|
||||
if (_isLoggingOut) return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _sig is null)
|
||||
return;
|
||||
|
||||
_isLoggingOut = true;
|
||||
_submitConfirmVisible = false;
|
||||
_submitErrorMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!);
|
||||
Navigation.NavigateTo("/", forceLoad: true);
|
||||
|
||||
try
|
||||
{
|
||||
if (_signatures.Count == 0)
|
||||
throw new InvalidOperationException("No signature fields found for receiver.");
|
||||
|
||||
var payload = _signatures.Select(field => new SignatureSubmitDto
|
||||
{
|
||||
Id = field.Id,
|
||||
DataUrl = _sig.DataUrl,
|
||||
FullName = _sig.FullName,
|
||||
Position = string.IsNullOrWhiteSpace(_sig.Position) ? null : _sig.Position,
|
||||
Place = _sig.Place
|
||||
}).ToList();
|
||||
|
||||
await SignatureService.SubmitAsync(EnvelopeKey, payload);
|
||||
|
||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||||
Navigation.NavigateTo("/", forceLoad: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to finalize signature for {EnvelopeKey}", EnvelopeKey);
|
||||
_submitErrorMessage = "Die Unterschrift konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.";
|
||||
_isLoggingOut = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
|
||||
@@ -27,8 +27,6 @@ namespace EnvelopeGenerator.Server.Controllers;
|
||||
[Authorize]
|
||||
public class EnvelopeController : ControllerBase
|
||||
{
|
||||
public record DeleteEnvelopeRequest(string Reason);
|
||||
|
||||
private readonly ILogger<EnvelopeController> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -110,17 +108,4 @@ public class EnvelopeController : ControllerBase
|
||||
else
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||
{
|
||||
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
||||
{
|
||||
EnvelopeId = id,
|
||||
Reason = request?.Reason ?? string.Empty
|
||||
}.WithAuth(User.GetId()));
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -18,13 +21,15 @@ namespace EnvelopeGenerator.Server.Controllers;
|
||||
public class SignatureController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<SignatureController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
||||
/// </summary>
|
||||
public SignatureController(IMediator mediator)
|
||||
public SignatureController(IMediator mediator, ILogger<SignatureController> logger)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
//TODO: update to use signature query
|
||||
@@ -54,4 +59,46 @@ public class SignatureController : ControllerBase
|
||||
else
|
||||
return Ok(rcvSignatures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the receiver signing flow and persists signature data to TBSIG_DOCUMENT_RECEIVER_ELEMENT.
|
||||
/// Also triggers status/history/mail side effects through SigningCommand pipeline.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("submit/{envelopeKey}")]
|
||||
public async Task<IActionResult> Submit(
|
||||
[FromRoute] string envelopeKey,
|
||||
[FromBody] IEnumerable<Signature>? signatures,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
if (signatures is null)
|
||||
return BadRequest("Signature payload is required.");
|
||||
|
||||
var uuid = User.EnvelopeUuid();
|
||||
var receiverSignature = User.ReceiverSignature();
|
||||
|
||||
if (await _mediator.IsSignedAsync(uuid, receiverSignature, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||
|
||||
try
|
||||
{
|
||||
await _mediator.Send(new SigningCommand
|
||||
{
|
||||
Envelope = new() { Uuid = uuid },
|
||||
Receiver = new() { Signature = receiverSignature },
|
||||
ReceiverAppType = ReceiverAppType.ReceiverUI,
|
||||
Signatures = signatures.ToList()
|
||||
}, cancel);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to submit signatures for envelope {EnvelopeKey}", envelopeKey);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +69,6 @@ public class ExceptionHandlingMiddleware
|
||||
message = notFoundEx.Message;
|
||||
break;
|
||||
|
||||
case ForbiddenException forbiddenEx:
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
message = forbiddenEx.Message;
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.LogError(exception, "Unhandled exception occurred.");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||
"applicationUrl": "https://localhost:8089;http://localhost:5132",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"TimeLimit": "00:30:00"
|
||||
},
|
||||
"DbTriggerParams": {
|
||||
"DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ],
|
||||
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||
"EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||
"History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
||||
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace EnvelopeGenerator.Tests.Application;
|
||||
|
||||
[TestFixture]
|
||||
public class DeleteEnvelopeCommandTests : TestBase
|
||||
{
|
||||
protected override void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteEnvelope_DraftEnvelope_ShouldSetDeleted_CreateHistory_AndQueueDeletionEmail()
|
||||
{
|
||||
CancellationToken cancel = default;
|
||||
var reason = "Invalid receiver data";
|
||||
|
||||
var envelope = this.CreateEnvelope(User.Id);
|
||||
envelope.Status = EnvelopeStatus.EnvelopeCreated;
|
||||
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
|
||||
|
||||
var createReceiverCmd = this.CreateReceiverCommand();
|
||||
(var receiver, _) = await Mediator.Send(createReceiverCmd, cancel);
|
||||
|
||||
var envReceiver = this.CreateEnvelopeReceiver(envelope.Id, receiver.Id);
|
||||
await GetRepository<EnvelopeReceiver>().CreateAsync(envReceiver, cancel);
|
||||
|
||||
var result = await Mediator.Send(new DeleteEnvelopeCommand
|
||||
{
|
||||
EnvelopeId = envelope.Id,
|
||||
Reason = reason
|
||||
}.WithAuth(User.Id), cancel);
|
||||
|
||||
Assert.That(result.EnvelopeId, Is.EqualTo(envelope.Id));
|
||||
Assert.That(result.AppliedStatus, Is.EqualTo(EnvelopeStatus.EnvelopeDeleted));
|
||||
Assert.That(result.IsNoOp, Is.False);
|
||||
|
||||
var histories = await GetRepository<History>().Query
|
||||
.Where(h => h.EnvelopeId == envelope.Id && h.Status == EnvelopeStatus.EnvelopeDeleted)
|
||||
.ToListAsync(cancel);
|
||||
Assert.That(histories, Has.Count.EqualTo(1));
|
||||
Assert.That(histories[0].Comment, Is.EqualTo(reason));
|
||||
|
||||
var emails = await GetRepository<EmailOut>().Query
|
||||
.Where(e => e.WfId == (int)EnvelopeStatus.MessageDeletionSent && e.ReferenceId == receiver.Id)
|
||||
.ToListAsync(cancel);
|
||||
Assert.That(emails, Has.Count.EqualTo(1));
|
||||
Assert.That(emails[0].EmailAddress, Is.EqualTo(receiver.EmailAddress));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteEnvelope_SentEnvelope_ShouldSetWithdrawn()
|
||||
{
|
||||
CancellationToken cancel = default;
|
||||
|
||||
var envelope = this.CreateEnvelope(User.Id);
|
||||
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
||||
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
|
||||
|
||||
var result = await Mediator.Send(new DeleteEnvelopeCommand
|
||||
{
|
||||
EnvelopeId = envelope.Id,
|
||||
Reason = "Sender requested cancellation"
|
||||
}.WithAuth(User.Id), cancel);
|
||||
|
||||
Assert.That(result.AppliedStatus, Is.EqualTo(EnvelopeStatus.EnvelopeWithdrawn));
|
||||
|
||||
var histories = await GetRepository<History>().Query
|
||||
.Where(h => h.EnvelopeId == envelope.Id && h.Status == EnvelopeStatus.EnvelopeWithdrawn)
|
||||
.ToListAsync(cancel);
|
||||
Assert.That(histories, Has.Count.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeleteEnvelope_MissingReason_ShouldThrowBadRequest()
|
||||
{
|
||||
Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await Mediator.Send(new DeleteEnvelopeCommand
|
||||
{
|
||||
EnvelopeId = 123,
|
||||
Reason = " "
|
||||
}.WithAuth(User.Id)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteEnvelope_WrongOwner_ShouldThrowForbidden()
|
||||
{
|
||||
CancellationToken cancel = default;
|
||||
|
||||
var envelope = this.CreateEnvelope(User.Id);
|
||||
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
|
||||
|
||||
var otherUserCmd = this.CreateUserCommand();
|
||||
var otherUser = await GetRepository<DigitalData.UserManager.Domain.Entities.User>().CreateAsync(otherUserCmd, cancel);
|
||||
|
||||
Assert.ThrowsAsync<ForbiddenException>(async () =>
|
||||
await Mediator.Send(new DeleteEnvelopeCommand
|
||||
{
|
||||
EnvelopeId = envelope.Id,
|
||||
Reason = "Not allowed"
|
||||
}.WithAuth(otherUser.Id), cancel));
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class Fake
|
||||
|
||||
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
||||
{
|
||||
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase($"EnvelopeGeneratorTestDb_{Guid.NewGuid():N}"));
|
||||
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
|
||||
|
||||
opt.AddDbTriggerParams(context.Configuration);
|
||||
|
||||
@@ -308,4 +308,4 @@ public static class Extensions
|
||||
.Select(_ => fake.CreateUserCommand())
|
||||
.ToList();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,7 @@ public abstract class TestBase : Faker
|
||||
|
||||
// Add seed email templates
|
||||
foreach (var temp in SeedEmailTemplates)
|
||||
{
|
||||
temp.LangCode ??= "de";
|
||||
await repo.CreateAsync(temp);
|
||||
}
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
@@ -143,4 +140,4 @@ public abstract class TestBase : Faker
|
||||
ChangedWhen = DateTime.Parse("2025-05-12 10:43:44.290")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.DependencyInjection", "EnvelopeGenerator.DependencyInjection\EnvelopeGenerator.DependencyInjection.csproj", "{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
|
||||
@@ -95,10 +93,6 @@ Global
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -136,7 +130,6 @@ Global
|
||||
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{BF1700D5-592E-4FFA-84E8-5480E289A1F0} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{4E6C54DA-576D-0955-2564-9EC890BB8279} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0}
|
||||
|
||||
Reference in New Issue
Block a user