using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Common; using EnvelopeGenerator.Web.Services; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using DigitalData.Core.API; using EnvelopeGenerator.Extensions; using Microsoft.Extensions.Localization; using DigitalData.Core.DTO; using Microsoft.AspNetCore.Localization; using EnvelopeGenerator.Web.Models; using EnvelopeGenerator.Application.Resources; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; using static EnvelopeGenerator.Common.Constants; using Ganss.Xss; using Newtonsoft.Json; using EnvelopeGenerator.Application.DTOs; using DigitalData.Core.Client; using System.Text.Json; using EnvelopeGenerator.Application.Extensions; namespace EnvelopeGenerator.Web.Controllers { public class HomeController : Controller { private readonly ILogger _logger; private readonly EnvelopeOldService envelopeOldService; private readonly IEnvelopeReceiverService _envRcvService; private readonly IEnvelopeHistoryService _historyService; private readonly IStringLocalizer _localizer; private readonly IConfiguration _configuration; private readonly HtmlSanitizer _sanitizer; private readonly Cultures _cultures; private readonly IEnvelopeMailService _mailService; private readonly IEnvelopeReceiverReadOnlyService _readOnlyService; private readonly IMessagingService _msgService; private readonly IEnvelopeReceiverCache _erCache; private readonly ICodeGenerator _codeGenerator; private readonly IReceiverService _rcvService; public HomeController(EnvelopeOldService envelopeOldService, ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IMessagingService messagingService, IEnvelopeReceiverCache envelopeReceiverCache, ICodeGenerator codeGenerator, IReceiverService receiverService) { this.envelopeOldService = envelopeOldService; _envRcvService = envelopeReceiverService; _historyService = historyService; _localizer = localizer; _configuration = configuration; _sanitizer = sanitizer; _cultures = cultures; _mailService = envelopeMailService; _logger = logger; _readOnlyService = readOnlyService; _msgService = messagingService; _erCache = envelopeReceiverCache; _codeGenerator = codeGenerator; _rcvService = receiverService; } [HttpGet("/")] public IActionResult Main([FromQuery] string? culture = null) { //TODO: add a middelware or use an asp.net functionality insead of this code-smell culture = culture is not null ? _sanitizer.Sanitize(culture) : null; if (UserLanguage is null && culture is null) { UserLanguage = _cultures.Default.Language; return Redirect($"{Request.Headers["Referer"]}?culture={_cultures.Default.Language}"); } ViewData["UserCulture"] = _cultures[UserLanguage]; return View(); } [HttpGet("EnvelopeKey/{envelopeReceiverId}")] public async Task MainAsync([FromRoute] string envelopeReceiverId, [FromQuery] string? culture = null) { try { //TODO: add a middelware or use an asp.net functionality insead of this code-smell culture = culture is not null ? _sanitizer.Sanitize(culture) : null; envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId); if (UserLanguage is null && culture is null) { UserLanguage = _cultures.Default.Language; return Redirect($"{Request.Headers["Referer"]}?culture={_cultures.Default.Language}"); } envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId); if (!envelopeReceiverId.TryDecode(out var decoded)) { Response.StatusCode = StatusCodes.Status401Unauthorized; return this.ViewDocumentNotFound(); } if(decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly) return Redirect($"{envelopeReceiverId}/ReadOnly"); ViewData["EnvelopeKey"] = envelopeReceiverId; return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync( SuccessAsync: async er => { EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId); bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress); if (!accessCodeAlreadyRequested) { await _historyService.RecordAsync(er.EnvelopeId, er.Receiver.EmailAddress, Constants.EnvelopeStatus.AccessCodeRequested); var mailRes = await _mailService.SendAccessCodeAsync(envelopeReceiverDto: er); if (mailRes.IsFailed) { _logger.LogNotice(mailRes); return this.ViewAccessCodeNotSent(); } } return Redirect($"{envelopeReceiverId}/Locked"); }, Fail: (messages, notices) => { _logger.LogNotice(notices); return this.ViewEnvelopeNotFound(); }); } catch(Exception ex) { _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception:ex, message: _localizer[WebKey.UnexpectedError]); return this.ViewInnerServiceError(); } } [HttpGet("EnvelopeKey/{envelopeReceiverId}/Locked")] public async Task EnvelopeLocked([FromRoute] string envelopeReceiverId) { try { ViewData["UserCulture"] = _cultures[UserLanguage]; return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync( Success: er => View() .WithData("EnvelopeKey", envelopeReceiverId) .WithData("TFAEnabled", er.TFAEnabled) .WithData("HasPhoneNumber", er.HasPhoneNumber), Fail: IActionResult (messages, notices) => { _logger.LogNotice(notices); Response.StatusCode = StatusCodes.Status401Unauthorized; return this.ViewEnvelopeNotFound(); }); } catch(Exception ex) { _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex); return this.ViewInnerServiceError(); } } [HttpPost("EnvelopeKey/{envelopeReceiverId}/Locked")] public async Task LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth) { try { ViewData["UserCulture"] = _cultures[UserLanguage]; envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId); (string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId(); if (uuid is null || signature is null) { _logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[WebKey.WrongEnvelopeReceiverId]); return Unauthorized(); } _logger.LogInformation("Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]", uuid, signature); //check access code EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId); return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync( SuccessAsync: async er_secret => { async Task TFAView(bool viaSms) { if (viaSms) { var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, envelopeReceiverId: envelopeReceiverId); if (res.Ok) return View("EnvelopeLocked").WithData("AccessCodeName", "smsCode").WithData("Expiration", res.Expiration); else if (!res.Allowed) return View("EnvelopeLocked").WithData("AccessCodeName", "smsCode").WithData("Expiration", res.AllowedAt); else { var res_json = JsonConvert.SerializeObject(res); _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: $"An unexpected error occurred while sending an SMS code. Response: ${res_json}"); return this.ViewInnerServiceError(); } } else { return View("EnvelopeLocked").WithData("AccessCodeName", "authenticatorCode"); } } if (auth.HasMulti) { Response.StatusCode = StatusCodes.Status401Unauthorized; return View("EnvelopeLocked") .WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value); } else if (auth.HasAccessCode) { //check the access code verification if (er_secret.AccessCode != auth.AccessCode) { //Constants.EnvelopeStatus.AccessCodeIncorrect await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, Constants.EnvelopeStatus.AccessCodeIncorrect); Response.StatusCode = StatusCodes.Status401Unauthorized; return View("EnvelopeLocked") .WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value); } await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, Constants.EnvelopeStatus.AccessCodeCorrect); //check if the user has phone is added if (er_secret.TFAEnabled) { var rcv = er_secret.Receiver; if (rcv.IsTotpSecretInvalid()) { rcv.TotpSecretkey = _codeGenerator.GenerateTotpSecretKey(); rcv.TotpExpiration = DateTime.Now.AddMonths(1); await _rcvService.UpdateAsync(rcv); await _mailService.SendTFAQrCodeAsync(er_secret); } return await TFAView(auth.UserSelectSMS); } } else if (auth.HasSmsCode) { var smsCode = await _erCache.GetSmsCodeAsync(envelopeReceiverId); if (smsCode is null) return RedirectToAction("EnvelopeLocked", new { envelopeReceiverId }); if(auth.SmsCode != smsCode) { Response.StatusCode = StatusCodes.Status401Unauthorized; ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value; return await TFAView(viaSms: true); } } else { Response.StatusCode = StatusCodes.Status401Unauthorized; return View("EnvelopeLocked") .WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value); } //continue the process without important data to minimize security errors. EnvelopeReceiverDto er = er_secret; ViewData["EnvelopeKey"] = envelopeReceiverId; //check rejection var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id); if(rejRcvrs.Any()) { 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)) return View("EnvelopeSigned"); if (er.Envelope.Documents?.FirstOrDefault() is EnvelopeDocumentDto 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(); } var claims = new List { new(ClaimTypes.NameIdentifier, uuid), new(ClaimTypes.Hash, signature), new(ClaimTypes.Name, er.Name ?? string.Empty), new(ClaimTypes.Email, er.Receiver.EmailAddress), new(EnvelopeClaimTypes.Title, er.Envelope.Title), new(EnvelopeClaimTypes.Id, er.Envelope.Id.ToString()) }; var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { AllowRefresh = false, IsPersistent = false }; await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); //add PSPDFKit licence key ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"]; return View("ShowEnvelope", er); }, Fail: (messages, notices) => { _logger.LogNotice(notices); return this.ViewEnvelopeNotFound(); } ); } catch (Exception ex) { _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex); return this.ViewInnerServiceError(); } } [Authorize] [HttpGet("EnvelopeKey/{envelopeReceiverId}/Success")] public async Task EnvelopeSigned(string envelopeReceiverId) { try { envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId); return await _envRcvService.IsExisting(envelopeReceiverId: envelopeReceiverId).ThenAsync( SuccessAsync: async isExisting => { if(!isExisting) return this.ViewEnvelopeNotFound(); EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId); if (!envelopeOldService.ReceiverAlreadySigned(response.Envelope, response.Receiver.Id)) return Redirect($"/EnvelopeKey/{envelopeReceiverId}/Locked"); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); ViewData["UserCulture"] = _cultures[UserLanguage]; ViewData["EnvelopeKey"] = envelopeReceiverId; return View(); }, Fail: IActionResult (messages, notices) => { _logger.LogNotice(notices); return this.ViewEnvelopeNotFound(); }); } catch (Exception ex) { _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex); return this.ViewInnerServiceError(); } } [Authorize] [HttpGet("EnvelopeKey/{envelopeReceiverId}/Rejected")] public async Task EnvelopeRejected(string envelopeReceiverId) { try { envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId).ThenAsync( SuccessAsync: async (er) => {ViewData["UserCulture"] = _cultures[UserLanguage]; ViewData["UserCulture"] = _cultures[UserLanguage]; return await _historyService.IsRejected(envelopeId: er.EnvelopeId) ? View(er) : Redirect($"/EnvelopeKey/{envelopeReceiverId}/Locked"); }, Fail: IActionResult (messages, notices) => { _logger.LogNotice(notices); return this.ViewEnvelopeNotFound(); }); } catch (Exception ex) { _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex); return this.ViewInnerServiceError(); } } [HttpGet("EnvelopeKey/{readOnlyKey}/ReadOnly")] public async Task EnvelopeReceiverReadOnly([FromRoute] string readOnlyKey) { try { ViewData["UserCulture"] = _cultures[UserLanguage]; readOnlyKey = _sanitizer.Sanitize(readOnlyKey); // check if the readOnlyId is valid if (!readOnlyKey.TryDecode(out var decodedKeys) || decodedKeys.GetEncodeType() != EncodeType.EnvelopeReceiverReadOnly) { Response.StatusCode = StatusCodes.Status401Unauthorized; return this.ViewDocumentNotFound(); } var readOnlyId = decodedKeys.ParseReadOnlyId(); 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).EncodeEnvelopeReceiverId(); EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeKey); //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-key: {readOnlyKey}\nEnvelope Receiver:\n{envelopeReceiver}", readOnlyKey, JsonConvert.SerializeObject(er)); _logger.LogNotice(hist_res.Notices); } if (er.Envelope.Documents?.FirstOrDefault() is EnvelopeDocumentDto 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(); }); } catch (Exception ex) { _logger.LogError(ex, "An unexpected error occurred while displaying a read-only envelope. Read-only key is {readOnlyKey}. {message}", readOnlyKey, ex.Message); return this.ViewInnerServiceError(); } } [Authorize] [HttpGet("IsAuthenticated")] public IActionResult IsAuthenticated() { var envelopeUuid = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var receiverSignature = User.FindFirst(ClaimTypes.Hash)?.Value; return Ok(new { EnvelopeUuid = envelopeUuid, ReceiverSignature = receiverSignature }); } [HttpPost("lang/{language}")] public IActionResult SetLanguage([FromRoute] string language) { try { language = _sanitizer.Sanitize(language); if (!_cultures.Languages.Contains(language)) return BadRequest(); UserLanguage = language; return Redirect(Request.Headers["Referer"].ToString()); } catch(Exception ex) { _logger.LogError(ex, "{Message}", ex.Message); return StatusCode(statusCode: StatusCodes.Status500InternalServerError); } } [HttpGet("lang")] public IActionResult GetLanguages() => Ok(_cultures.Languages); private string? UserLanguage { get { var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName]; if (string.IsNullOrEmpty(cookieValue)) return null; var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0]; return culture?.Value ?? null; } set { if(value is null) Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName); else { var cookieOptions = new CookieOptions() { Expires = DateTimeOffset.UtcNow.AddYears(1), Secure = false, SameSite = SameSiteMode.Strict, HttpOnly = true }; Response.Cookies.Append( CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)), cookieOptions); } } } public IActionResult Error404() => this.ViewError404(); } }