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 logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer 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 Main([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(); #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 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 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 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); 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 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 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 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 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 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 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(); }); } }