Compare commits

...

15 Commits

Author SHA1 Message Date
a8a73724e6 refactor(EnvelopeController): add logic to check the claims with role 2025-09-18 20:48:09 +02:00
8059e0aae4 remove old routes 2025-09-18 19:53:09 +02:00
4c5116695b remove envelope receiver id input 2025-09-18 19:51:43 +02:00
ffa31fbebc change LogInEnvelope route 2025-09-18 18:37:10 +02:00
e536e523b8 fix(EnvelopeController): rename CreateEnvelopeLockedView as EnvelopeLocked 2025-09-18 18:35:37 +02:00
1d64c19605 add cancelation token 2025-09-18 18:19:22 +02:00
29e6ba8733 Refactoring (EnvelopeController): Vereinheitlichung der Verarbeitung von gesperrten Ansichten
- Methode „CreateEnvelopeLockedView” hinzugefügt, um die Logik für die Anzeige gesperrter/umschlossener Ansichten zu zentralisieren
- Aktion „Main“ aktualisiert, um „CreateEnvelopeLockedView“ anstelle von Redirect zu verwenden
- Veraltete Methode „EnvelopeLocked“ entfernt
- TFA-bezogene Ansichten und Handler aktualisiert, um auf „CreateEnvelopeLockedView“ anstelle von „EnvelopeLocked“ zu verweisen
- Dokumentenlade- und Anmeldelogik in wiederverwendbare Methoden vereinfacht
2025-09-18 18:18:59 +02:00
2e6eeafd74 refactor(EnvelopeController): add read-only region 2025-09-18 17:22:40 +02:00
643501f484 refactor(EnvelopeController): simplify EnvelopeController actions and view handling
- Added overload of CreateShowEnvelopeView(EnvelopeReceiverDto) to remove redundant id parsing logic
- Refactored EnvelopeLocked action to receive EnvelopeReceiverDto directly instead of fetching inside
- Centralized ViewData assignments for cleaner envelope rendering
- Improved error logging to include envelope UUID and receiver signature where applicable
- Adjusted EnvelopeLocked GET to return explicit "EnvelopeLocked" view for unauthenticated users
2025-09-18 17:14:28 +02:00
0c900d219c refactor(EnvelopeController): add Send Access Code-region 2025-09-18 16:19:40 +02:00
8d8757810c refactor(EnvelopeController): move rejection and signing check to Main-endpoint 2025-09-18 16:15:33 +02:00
9dec681ce5 Home und View in Home und Envelope unterteilen 2025-09-18 14:01:21 +02:00
01ec2b3df2 refactor(HomeController): Als Home- und Envelope-Controller aufteilen 2025-09-18 13:16:33 +02:00
d64d46920a refactor(api-services): change wrong url route 2025-09-18 12:59:01 +02:00
6147a619a8 refactor(EnvelopeReceiverReadOnly): fix nullability problem 2025-09-18 12:33:00 +02:00
12 changed files with 428 additions and 413 deletions

View File

@@ -42,13 +42,21 @@ namespace EnvelopeGenerator.Domain.Entities
[Column("ADDED_WHEN")]
[Required]
public DateTime AddedWhen { get; set; }
public DateTime
#if NET
?
#endif
AddedWhen { get; set; }
[Column("CHANGED_WHO")]
[StringLength(100)]
public string ChangedWho { get; set; }
public string
#if NET
?
#endif
ChangedWho { get; set; }
[Column("CHANGED_WHEN")]
public DateTime ChangedWhen { get; set; }
public DateTime? ChangedWhen { get; set; }
}
}

View File

@@ -0,0 +1,406 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.API;
using DigitalData.Core.Client;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Web.Extensions;
using EnvelopeGenerator.Web.Models;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using OtpNet;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Web.Controllers;
[Route("Envelope")]
public class EnvelopeController : ViewControllerBase
{
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverService _envRcvService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeHistoryService _historyService;
private readonly IConfiguration _configuration;
[Obsolete("Use MediatR")]
private readonly IEnvelopeMailService _mailService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
private readonly IAuthenticator _authenticator;
[Obsolete("Use MediatR")]
private readonly IReceiverService _rcvService;
private readonly IEnvelopeSmsHandler _envSmsHandler;
private readonly IMediator _mediator;
[Obsolete("Use MediatR")]
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IAuthenticator authenticator, IReceiverService receiverService, IEnvelopeSmsHandler envelopeSmsService, IMediator mediator) : base(logger, cultures, localizer)
{
_envRcvService = envelopeReceiverService;
_historyService = historyService;
_configuration = configuration;
_mailService = envelopeMailService;
_readOnlyService = readOnlyService;
_authenticator = authenticator;
_rcvService = receiverService;
_envSmsHandler = envelopeSmsService;
_mediator = mediator;
}
[HttpGet("{envelopeReceiverId}")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> Main([FromRoute] string envelopeReceiverId, CancellationToken cancel)
{
try
{
ViewData["EnvelopeKey"] = envelopeReceiverId;
if (!envelopeReceiverId.TryDecode(out var decoded))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
return this.ViewDocumentNotFound();
}
#region Read Only
if (decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly)
return await EnvelopeReceiverReadOnly(decoded.ParseReadOnlyId());
#endregion
var er = await _mediator.ReadEnvelopeReceiverAsync(envelopeReceiverId, cancel);
if (er is null)
return this.ViewEnvelopeNotFound();
#region Rejected or Signed
//check rejection
var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id);
if (rejRcvrs.Any())
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
ViewBag.IsExt = !rejRcvrs.Contains(er.Receiver); //external if the current user is not rejected
return View("EnvelopeRejected", er);
}
//check if it has already signed
if (await _historyService.IsSigned(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return View("EnvelopeSigned");
}
#endregion
#region Send Access Code
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress);
if (!accessCodeAlreadyRequested)
{
await _historyService.RecordAsync(er.EnvelopeId, er.Receiver.EmailAddress, EnvelopeStatus.AccessCodeRequested);
var mailRes = await _mailService.SendAccessCodeAsync(envelopeReceiverDto: er);
if (mailRes.IsFailed)
{
_logger.LogNotice(mailRes);
return this.ViewAccessCodeNotSent();
}
}
#endregion
return await CreateEnvelopeLockedView(er, cancel);
}
catch(Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception:ex, message: _localizer.UnexpectedError());
return this.ViewInnerServiceError();
}
}
[HttpPost("{envelopeReceiverId}")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth)
{
try
{
ViewData["EnvelopeKey"] = envelopeReceiverId;
(string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
return Unauthorized();
}
_logger.LogInformation("Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]", uuid, signature);
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
if (er_secret_res.IsFailed)
{
_logger.LogNotice(er_secret_res.Notices);
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
// show envelope if already logged in
if (User.IsInRole(ReceiverRole.FullyAuth))
return await CreateShowEnvelopeView(er_secret);
if (auth.HasMulti)
{
return Unauthorized();
}
else if (auth.HasAccessCode)
{
if (await HandleAccessCodeAsync(auth, er_secret, envelopeReceiverId) is IActionResult acView)
return acView;
}
else if (auth.HasSmsCode)
{
if (await HandleSmsAsync(auth, er_secret, envelopeReceiverId) is IActionResult smsView)
return smsView;
}
else if (auth.HasAuthenticatorCode)
{
if (await HandleAuthenticatorAsync(auth, er_secret, envelopeReceiverId) is IActionResult aView)
return aView;
}
else
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("EnvelopeKey", envelopeReceiverId)
.WithData("TFAEnabled", er_secret.Envelope!.TFAEnabled)
.WithData("HasPhoneNumber", er_secret.HasPhoneNumber)
.WithData("SenderEmail", er_secret.Envelope.User!.Email)
.WithData("EnvelopeTitle", er_secret.Envelope.Title)
.WithData("ErrorMessage", _localizer.WrongEnvelopeReceiverId());
}
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
return await CreateShowEnvelopeView(er_secret);
}
catch (Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex);
return this.ViewInnerServiceError();
}
}
private async Task<IActionResult> CreateEnvelopeLockedView(EnvelopeReceiverDto er, CancellationToken cancel)
{
var uuidClaim = User.GetAuthEnvelopeUuid();
var signatureClaim = User.GetAuthReceiverSignature();
if (uuidClaim is not null
&& uuidClaim == er.Envelope?.Uuid
&& signatureClaim is not null
&& signatureClaim == er.Receiver?.Signature
&& User.IsInRole(ReceiverRole.FullyAuth))
{
if (er.Envelope!.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
ViewData["DocumentBytes"] = doc.ByteData;
}
else
{
_logger.LogEnvelopeError(uuid: er.Envelope.Uuid, er.Receiver?.Signature, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
return this.ViewDocumentNotFound();
}
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
return View("ShowEnvelope", er);
}
else
{
ViewData["TFAEnabled"] = er.Envelope!.TFAEnabled;
ViewData["HasPhoneNumber"] = er.HasPhoneNumber;
ViewData["SenderEmail"] = er.Envelope.User!.Email;
ViewData["EnvelopeTitle"] = er.Envelope.Title;
return View("EnvelopeLocked");
}
}
private async Task<IActionResult> CreateShowEnvelopeView(EnvelopeReceiverDto er)
{
if (er.Envelope!.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
ViewData["DocumentBytes"] = doc.ByteData;
}
else
{
_logger.LogEnvelopeError(er.Envelope.Uuid, er.Receiver?.Signature, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
return this.ViewDocumentNotFound();
}
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
return View("ShowEnvelope", er);
}
#region TFA Views
[NonAction]
private async Task<IActionResult> TFAViewAsync(bool viaSms, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
if (viaSms)
{
var (smsRes, expiration) = await _envSmsHandler.SendTotpAsync(er_secret);
ViewData["EnvelopeKey"] = envelopeReceiverId;
ViewData["TFAEnabled"] = er_secret.Envelope!.TFAEnabled;
ViewData["HasPhoneNumber"] = er_secret.HasPhoneNumber;
ViewData["SenderEmail"] = er_secret.Envelope.User!.Email;
ViewData["EnvelopeTitle"] = er_secret.Envelope.Title;
if (smsRes?.Failed ?? false)
{
var res_json = JsonConvert.SerializeObject(smsRes);
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: $"An unexpected error occurred while sending an SMS code. Response: ${res_json}");
return this.ViewInnerServiceError();
}
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", expiration);
}
else
{
return View("EnvelopeLocked")
.WithData("CodeType", "authenticatorCode")
.WithData("TfaRegDeadline", er_secret.Receiver?.TfaRegDeadline);
}
}
[Obsolete("Use MediatR")]
[NonAction]
private async Task<IActionResult?> HandleAccessCodeAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
//check the access code verification
if (er_secret.AccessCode != auth.AccessCode)
{
//EnvelopeStatusQuery.AccessCodeIncorrect
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeIncorrect);
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("EnvelopeKey", envelopeReceiverId)
.WithData("TFAEnabled", er_secret.Envelope!.TFAEnabled)
.WithData("HasPhoneNumber", er_secret.HasPhoneNumber)
.WithData("SenderEmail", er_secret.Envelope.User!.Email)
.WithData("EnvelopeTitle", er_secret.Envelope.Title)
.WithData("ErrorMessage", _localizer.WrongAccessCode());
}
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeCorrect);
//check if the user has phone is added
if (er_secret.Envelope!.TFAEnabled)
{
var rcv = er_secret.Receiver;
if (rcv.TotpSecretkey is null)
{
rcv.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
await _rcvService.UpdateAsync(rcv);
}
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.PreAuth);
return await TFAViewAsync(auth.UserSelectSMS, er_secret, envelopeReceiverId);
}
return null;
}
[NonAction]
private async Task<IActionResult?> HandleSmsAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (!User.IsInRole(ReceiverRole.PreAuth) || !_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer.WrongAccessCode();
return await TFAViewAsync(viaSms: true, er_secret, envelopeReceiverId);
}
return null;
}
[NonAction]
private async Task<IActionResult?> HandleAuthenticatorAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (!User.IsInRole(ReceiverRole.PreAuth) || !_authenticator.VerifyTotp(auth.AuthenticatorCode!, er_secret.Receiver.TotpSecretkey, window: VerificationWindow.RfcSpecifiedNetworkDelay))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer.WrongAccessCode();
return await TFAViewAsync(viaSms: false, er_secret, envelopeReceiverId);
}
return null;
}
#endregion
[NonAction]
[Obsolete("Use MediatR")]
public async Task<IActionResult> EnvelopeReceiverReadOnly([FromRoute] long readOnlyId)
{
var erro_res = await _readOnlyService.ReadByIdAsync(readOnlyId);
if (erro_res.IsFailed)
{
_logger.LogNotice(erro_res.Notices);
return this.ViewInnerServiceError();
}
var erro = erro_res.Data;
if (DateTime.Now > erro.DateValid)
return View("EnvelopeExpired");
return await _envRcvService.ReadByUuidSignatureAsync(uuid: erro.Envelope!.Uuid, erro.Receiver!.Signature).ThenAsync(
SuccessAsync: async er =>
{
var envelopeKey = (er.Envelope!.Uuid, er.Receiver!.Signature).ToEnvelopeKey();
//TODO: implement multi-threading to history process (Task)
var hist_res = await _historyService.RecordAsync((int)erro.EnvelopeId, erro.AddedWho, EnvelopeStatus.EnvelopeViewed);
if (hist_res.IsFailed)
{
_logger.LogError(
"Although the envelope was sent as read-only, the EnvelopeShared hisotry could not be saved. ReadOnly-Id: {readOnlyKey}\nEnvelope Receiver:\n{envelopeReceiver}",
readOnlyId, JsonConvert.SerializeObject(er));
_logger.LogNotice(hist_res.Notices);
}
if (er.Envelope.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
ViewData["DocumentBytes"] = doc.ByteData;
ViewData["EnvelopeKey"] = envelopeKey;
ViewData["IsReadOnly"] = true;
ViewData["ReadOnly"] = erro;
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
return View("ShowEnvelope", er);
}
else
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeKey, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
return this.ViewDocumentNotFound();
}
},
Fail: (messages, notices) =>
{
_logger.LogNotice(notices);
return this.ViewEnvelopeNotFound();
});
}
}

View File

@@ -1,60 +1,22 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.API;
using DigitalData.Core.Client;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Web.Extensions;
using EnvelopeGenerator.Web.Models;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using OtpNet;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Web.Controllers;
[Route("Envelope")]
[Route("/")]
public class HomeController : ViewControllerBase
{
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverService _envRcvService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeHistoryService _historyService;
private readonly IConfiguration _configuration;
[Obsolete("Use MediatR")]
private readonly IEnvelopeMailService _mailService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
private readonly IAuthenticator _authenticator;
[Obsolete("Use MediatR")]
private readonly IReceiverService _rcvService;
private readonly IEnvelopeSmsHandler _envSmsHandler;
private readonly IMediator _mediator;
[Obsolete("Use MediatR")]
public HomeController(ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IAuthenticator authenticator, IReceiverService receiverService, IEnvelopeSmsHandler envelopeSmsService, IMediator mediator) : base(logger, cultures, localizer)
public HomeController(IConfiguration configuration, ILogger<HomeController> logger, Cultures cultures, IStringLocalizer<Resource> localizer) : base(logger, cultures, localizer)
{
_envRcvService = envelopeReceiverService;
_historyService = historyService;
_configuration = configuration;
_mailService = envelopeMailService;
_readOnlyService = readOnlyService;
_authenticator = authenticator;
_rcvService = receiverService;
_envSmsHandler = envelopeSmsService;
_mediator = mediator;
}
[HttpGet("/")]
[HttpGet]
public IActionResult Main()
{
return View(new MainViewModel()
@@ -63,367 +25,6 @@ public class HomeController : ViewControllerBase
});
}
[HttpGet("{envelopeReceiverId}")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> MainAsync([FromRoute] string envelopeReceiverId)
{
try
{
if (!envelopeReceiverId.TryDecode(out var decoded))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
return this.ViewDocumentNotFound();
}
if(decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly)
return await EnvelopeReceiverReadOnly(decoded.ParseReadOnlyId());
ViewData["EnvelopeKey"] = envelopeReceiverId;
var er = await _mediator.ReadEnvelopeReceiverAsync(envelopeReceiverId);
if (er is null)
return this.ViewEnvelopeNotFound();
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress);
if (!accessCodeAlreadyRequested)
{
await _historyService.RecordAsync(er.EnvelopeId, er.Receiver.EmailAddress, EnvelopeStatus.AccessCodeRequested);
var mailRes = await _mailService.SendAccessCodeAsync(envelopeReceiverDto: er);
if (mailRes.IsFailed)
{
_logger.LogNotice(mailRes);
return this.ViewAccessCodeNotSent();
}
}
return Redirect($"{envelopeReceiverId}/Locked");
}
catch(Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception:ex, message: _localizer.UnexpectedError());
return this.ViewInnerServiceError();
}
}
[HttpGet("{envelopeReceiverId}/Locked")]
[Obsolete("Use DigitalData.Core.Exceptions and .Middleware")]
public async Task<IActionResult> EnvelopeLocked([FromRoute] string envelopeReceiverId, CancellationToken cancel)
{
try
{
var er = await _mediator.ReadEnvelopeReceiverAsync(envelopeReceiverId, cancel);
if (er is null)
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
return this.ViewEnvelopeNotFound();
}
if (User.IsInRole(ReceiverRole.FullyAuth))
return await CreateShowEnvelopeView(envelopeReceiverId, er);
else
{
ViewData["EnvelopeKey"] = envelopeReceiverId;
ViewData["TFAEnabled"] = er.Envelope!.TFAEnabled;
ViewData["HasPhoneNumber"] = er.HasPhoneNumber;
ViewData["SenderEmail"] = er.Envelope.User!.Email;
ViewData["EnvelopeTitle"] = er.Envelope.Title;
return View();
}
}
catch(Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex);
return this.ViewInnerServiceError();
}
}
[Obsolete("Use MediatR")]
private async Task<IActionResult> CreateShowEnvelopeView(string envelopeReceiverId, EnvelopeReceiverDto er)
{
try
{
ViewData["EnvelopeKey"] = envelopeReceiverId;
(string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
return Unauthorized();
}
_logger.LogInformation("Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]", uuid, signature);
//check rejection
var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id);
if (rejRcvrs.Any())
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
ViewBag.IsExt = !rejRcvrs.Contains(er.Receiver); //external if the current user is not rejected
return View("EnvelopeRejected", er);
}
//check if it has already signed
if (await _historyService.IsSigned(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return View("EnvelopeSigned");
}
if (er.Envelope.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
ViewData["DocumentBytes"] = doc.ByteData;
}
else
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
return this.ViewDocumentNotFound();
}
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
return View("ShowEnvelope", er);
}
catch (Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex);
return this.ViewInnerServiceError();
}
}
#region TFA Views
[NonAction]
private async Task<IActionResult> TFAViewAsync(bool viaSms, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
if (viaSms)
{
var (smsRes, expiration) = await _envSmsHandler.SendTotpAsync(er_secret);
ViewData["EnvelopeKey"] = envelopeReceiverId;
ViewData["TFAEnabled"] = er_secret.Envelope!.TFAEnabled;
ViewData["HasPhoneNumber"] = er_secret.HasPhoneNumber;
ViewData["SenderEmail"] = er_secret.Envelope.User!.Email;
ViewData["EnvelopeTitle"] = er_secret.Envelope.Title;
if (smsRes?.Failed ?? false)
{
var res_json = JsonConvert.SerializeObject(smsRes);
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: $"An unexpected error occurred while sending an SMS code. Response: ${res_json}");
return this.ViewInnerServiceError();
}
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", expiration);
}
else
{
return View("EnvelopeLocked")
.WithData("CodeType", "authenticatorCode")
.WithData("TfaRegDeadline", er_secret.Receiver?.TfaRegDeadline);
}
}
[Obsolete("Use MediatR")]
[NonAction]
private async Task<IActionResult?> HandleAccessCodeAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
//check the access code verification
if (er_secret.AccessCode != auth.AccessCode)
{
//EnvelopeStatusQuery.AccessCodeIncorrect
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeIncorrect);
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("EnvelopeKey", envelopeReceiverId)
.WithData("TFAEnabled", er_secret.Envelope!.TFAEnabled)
.WithData("HasPhoneNumber", er_secret.HasPhoneNumber)
.WithData("SenderEmail", er_secret.Envelope.User!.Email)
.WithData("EnvelopeTitle", er_secret.Envelope.Title)
.WithData("ErrorMessage", _localizer.WrongAccessCode());
}
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeCorrect);
//check if the user has phone is added
if (er_secret.Envelope!.TFAEnabled)
{
var rcv = er_secret.Receiver;
if (rcv.TotpSecretkey is null)
{
rcv.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
await _rcvService.UpdateAsync(rcv);
}
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.PreAuth);
return await TFAViewAsync(auth.UserSelectSMS, er_secret, envelopeReceiverId);
}
return null;
}
[NonAction]
private async Task<IActionResult?> HandleSmsAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (!User.IsInRole(ReceiverRole.PreAuth) || !_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer.WrongAccessCode();
return await TFAViewAsync(viaSms: true, er_secret, envelopeReceiverId);
}
return null;
}
[NonAction]
private async Task<IActionResult?> HandleAuthenticatorAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (!User.IsInRole(ReceiverRole.PreAuth) || !_authenticator.VerifyTotp(auth.AuthenticatorCode!, er_secret.Receiver.TotpSecretkey, window: VerificationWindow.RfcSpecifiedNetworkDelay))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer.WrongAccessCode();
return await TFAViewAsync(viaSms: false, er_secret, envelopeReceiverId);
}
return null;
}
#endregion
[HttpPost("{envelopeReceiverId}/Locked")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth)
{
try
{
ViewData["EnvelopeKey"] = envelopeReceiverId;
(string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
return Unauthorized();
}
_logger.LogInformation("Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]", uuid, signature);
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
if (er_secret_res.IsFailed)
{
_logger.LogNotice(er_secret_res.Notices);
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
// show envelope if already logged in
if (User.IsInRole(ReceiverRole.FullyAuth))
return await CreateShowEnvelopeView(envelopeReceiverId, er_secret);
if (auth.HasMulti)
{
return Unauthorized();
}
else if (auth.HasAccessCode)
{
if (await HandleAccessCodeAsync(auth, er_secret, envelopeReceiverId) is IActionResult acView)
return acView;
}
else if (auth.HasSmsCode)
{
if (await HandleSmsAsync(auth, er_secret, envelopeReceiverId) is IActionResult smsView)
return smsView;
}
else if (auth.HasAuthenticatorCode)
{
if(await HandleAuthenticatorAsync(auth, er_secret, envelopeReceiverId) is IActionResult aView)
return aView;
}
else
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("EnvelopeKey", envelopeReceiverId)
.WithData("TFAEnabled", er_secret.Envelope!.TFAEnabled)
.WithData("HasPhoneNumber", er_secret.HasPhoneNumber)
.WithData("SenderEmail", er_secret.Envelope.User!.Email)
.WithData("EnvelopeTitle", er_secret.Envelope.Title)
.WithData("ErrorMessage", _localizer.WrongEnvelopeReceiverId());
}
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
return await CreateShowEnvelopeView(envelopeReceiverId, er_secret);
}
catch (Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex);
return this.ViewInnerServiceError();
}
}
[NonAction]
[Obsolete("Use MediatR")]
public async Task<IActionResult> EnvelopeReceiverReadOnly([FromRoute] long readOnlyId)
{
var erro_res = await _readOnlyService.ReadByIdAsync(readOnlyId);
if (erro_res.IsFailed)
{
_logger.LogNotice(erro_res.Notices);
return this.ViewInnerServiceError();
}
var erro = erro_res.Data;
if (DateTime.Now > erro.DateValid)
return View("EnvelopeExpired");
return await _envRcvService.ReadByUuidSignatureAsync(uuid: erro.Envelope!.Uuid, erro.Receiver!.Signature).ThenAsync(
SuccessAsync: async er =>
{
var envelopeKey = (er.Envelope!.Uuid, er.Receiver!.Signature).ToEnvelopeKey();
//TODO: implement multi-threading to history process (Task)
var hist_res = await _historyService.RecordAsync((int)erro.EnvelopeId, erro.AddedWho, EnvelopeStatus.EnvelopeViewed);
if (hist_res.IsFailed)
{
_logger.LogError(
"Although the envelope was sent as read-only, the EnvelopeShared hisotry could not be saved. ReadOnly-Id: {readOnlyKey}\nEnvelope Receiver:\n{envelopeReceiver}",
readOnlyId, JsonConvert.SerializeObject(er));
_logger.LogNotice(hist_res.Notices);
}
if (er.Envelope.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
ViewData["DocumentBytes"] = doc.ByteData;
ViewData["EnvelopeKey"] = envelopeKey;
ViewData["IsReadOnly"] = true;
ViewData["ReadOnly"] = erro;
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
return View("ShowEnvelope", er);
}
else
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeKey, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
return this.ViewDocumentNotFound();
}
},
Fail: (messages, notices) =>
{
_logger.LogNotice(notices);
return this.ViewEnvelopeNotFound();
});
}
[HttpGet("Error404")]
public IActionResult Error404() => this.ViewError404();
}

View File

@@ -12,9 +12,9 @@
<PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Version>3.3.0</Version>
<AssemblyVersion>3.3.0</AssemblyVersion>
<FileVersion>3.3.0</FileVersion>
<Version>3.3.1</Version>
<AssemblyVersion>3.3.1</AssemblyVersion>
<FileVersion>3.3.1</FileVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>

View File

@@ -136,7 +136,7 @@ try
{
// Dynamically calculate the redirection path, for example:
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}/Locked";
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
@@ -145,7 +145,7 @@ try
{
// Apply a similar redirection logic for logout
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}/Success";
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;

View File

@@ -6,7 +6,7 @@ class Content {
class API {
static get REJECT_URL() {
return `/api/envelope/reject`;
return `/api/annotation/reject`;
}
static get REJECT_REDIR_URL() {

View File

@@ -1,2 +1,2 @@
class Content{static get JSON(){return"application/json"}}class API{static get REJECT_URL(){return`/api/envelope/reject`}static get REJECT_REDIR_URL(){return`/envelope/${API.ENV_KEY}`}static get SHARE_URL(){return`/api/readonly`}static __XSRF_TOKEN
class Content{static get JSON(){return"application/json"}}class API{static get REJECT_URL(){return`/api/annotation/reject`}static get REJECT_REDIR_URL(){return`/envelope/${API.ENV_KEY}`}static get SHARE_URL(){return`/api/readonly`}static __XSRF_TOKEN
static get XSRF_TOKEN(){return API.__XSRF_TOKEN??=document.getElementsByName("__RequestVerificationToken")[0].value,API.__XSRF_TOKEN}static get ENV_KEY(){return ENV_KEY??document.querySelector('meta[name="env-key"]').getAttribute("content")}}const submitForm=async n=>await fetch(n.action,{method:n.method,body:new FormData(n),headers:{"X-Requested-With":"XMLHttpRequest"}}),createRequest=async(n,t,i,r)=>fetch(t,{credentials:"include",method:n,headers:{"Content-Type":r,"X-XSRF-TOKEN":API.XSRF_TOKEN},body:JSON.stringify(i)}),createPost=(n,t,i)=>createRequest("POST",n,t,i),rejectEnvelope=n=>createPost(API.REJECT_URL,n,Content.JSON),redirect=n=>window.location.href=n,redirRejected=()=>redirect(API.REJECT_REDIR_URL),shareEnvelope=(n,t)=>createPost(API.SHARE_URL,{receiverMail:n,dateValid:t},Content.JSON);