Files
EnvelopeGenerator/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs
TekH 6b23dcdba7 Refactor: unify role constants under new Role class
Replaced all usages of ReceiverRole with the new Role class in EnvelopeGenerator.Domain.Constants. Removed ReceiverRole.cs and added Role.cs with PreAuth and FullyAuth constants. Updated all [Authorize] attributes and role checks in controllers and authentication logic to use Role.FullyAuth and Role.PreAuth. This centralizes role management for improved maintainability and clarity.
2026-02-02 11:53:26 +01:00

429 lines
18 KiB
C#

using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.API;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.PdfEditor;
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;
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 UseAccessCode
if (!er.Envelope!.UseAccessCode)
{
(string? uuid, string? signature) = decoded.ParseEnvelopeReceiverId();
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;
await HttpContext.SignInEnvelopeAsync(er_secret, Role.FullyAuth);
return await CreateShowEnvelopeView(er_secret);
}
#endregion UseAccessCode
#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, CancellationToken cancel)
{
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;
//check rejection
var rejRcvrs = await _historyService.ReadRejectingReceivers(er_secret.Envelope!.Id);
if (rejRcvrs.Any())
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
ViewBag.IsExt = !rejRcvrs.Contains(er_secret.Receiver); //external if the current user is not rejected
return View("EnvelopeRejected", er_secret);
}
// show envelope if already logged in
if (User.IsInRole(Role.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, Role.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(Role.FullyAuth))
{
await HttpContext.SignInEnvelopeAsync(er, Role.FullyAuth);
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
return await CreateShowEnvelopeView(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)
{
using var pdf = er.Envelope.ReadOnly
? Pdf.FromMemory(doc.ByteData)
: Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
doc.ByteData = pdf.ExportAsBytes();
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, Role.FullyAuth);
ViewData["ReadAndConfirm"] = er.Envelope.ReadOnly;
//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, Role.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(Role.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(Role.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");
var erRes = await _envRcvService.ReadByUuidSignatureAsync(uuid: erro.Envelope!.Uuid, erro.Receiver!.Signature);
if (erRes.IsFailed)
{
_logger.LogNotice(erRes.Notices);
return this.ViewEnvelopeNotFound();
}
var er = erRes.Data;
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();
}
}
}