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 EnvelopeGenerator.Application.Extensions; using Microsoft.Extensions.Caching.Distributed; using System.Globalization; using Microsoft.Extensions.Options; using EnvelopeGenerator.Application.Configurations; 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 ISmsSender _msgService; private readonly ICodeGenerator _codeGenerator; private readonly IReceiverService _rcvService; private readonly IDistributedCache _dCache; private readonly TotpSmsParams _totpSmsParams; public HomeController(EnvelopeOldService envelopeOldService, ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, ISmsSender messagingService, ICodeGenerator codeGenerator, IReceiverService receiverService, IDistributedCache distributedCache, IOptions totpSmsParamsOptions) { this.envelopeOldService = envelopeOldService; _envRcvService = envelopeReceiverService; _historyService = historyService; _localizer = localizer; _configuration = configuration; _sanitizer = sanitizer; _cultures = cultures; _mailService = envelopeMailService; _logger = logger; _readOnlyService = readOnlyService; _msgService = messagingService; _codeGenerator = codeGenerator; _rcvService = receiverService; _dCache = distributedCache; _totpSmsParams = totpSmsParamsOptions.Value; } [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, 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.Envelope!.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(); } } #region TFA Views [NonAction] private async Task TFAViewAsync(bool viaSms, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId) { if (viaSms) { //TODO: create a service (like EnvelopeSmsService) //add date time cache var key = string.Format(_totpSmsParams.Expiration.CacheKeyFormat, er_secret.EnvelopeId, er_secret.ReceiverId); var expiration = await _dCache.GetDateTimeAsync(key); if (expiration is null || expiration <= DateTime.Now) { var new_expiration = DateTime.Now.AddSeconds(_totpSmsParams.TotpStep); var totp = _codeGenerator.GenerateTotp(er_secret.Receiver!.TotpSecretkey!, _totpSmsParams.TotpStep); var msg = string.Format(_totpSmsParams.Format, totp, new_expiration.ToString(_totpSmsParams.Expiration.Format, _totpSmsParams.Expiration.CultureInfo)); var smsRes = await _msgService.SendSmsAsync(er_secret.PhoneNumber!, msg); if (smsRes.Failed) { 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("QRCodeExpiration", er_secret.Receiver?.TotpExpiration); } } [NonAction] private async Task HandleAccessCodeAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId) { //check the access code verification if (er_secret.AccessCode != auth.AccessCode) { //Constants.EnvelopeStatus.AccessCodeIncorrect await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, 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, EnvelopeStatus.AccessCodeCorrect); //check if the user has phone is added if (er_secret.Envelope!.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 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 (_codeGenerator.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey, step: _totpSmsParams.TotpStep)) { Response.StatusCode = StatusCodes.Status401Unauthorized; ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value; 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!.IsTotpInvalid(totp: auth.AuthenticatorCode!)) { Response.StatusCode = StatusCodes.Status401Unauthorized; ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value; return await TFAViewAsync(viaSms: false, er_secret, envelopeReceiverId); } return null; } #endregion [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); 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; if (auth.HasMulti) { Response.StatusCode = StatusCodes.Status401Unauthorized; return View("EnvelopeLocked") .WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value); } 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("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); } 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(); } }