517 lines
24 KiB
C#
517 lines
24 KiB
C#
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 DevExpress.Utils.About;
|
|
|
|
namespace EnvelopeGenerator.Web.Controllers
|
|
{
|
|
public class HomeController : Controller
|
|
{
|
|
private readonly ILogger<HomeController> _logger;
|
|
private readonly EnvelopeOldService envelopeOldService;
|
|
private readonly IEnvelopeReceiverService _envRcvService;
|
|
private readonly IEnvelopeHistoryService _historyService;
|
|
private readonly IStringLocalizer<Resource> _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;
|
|
|
|
public HomeController(EnvelopeOldService envelopeOldService, ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IMessagingService messagingService, IEnvelopeReceiverCache envelopeReceiverCache)
|
|
{
|
|
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;
|
|
}
|
|
|
|
[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<IActionResult> 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<EnvelopeReceiverDto, IActionResult>(
|
|
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<IActionResult> EnvelopeLocked([FromRoute] string envelopeReceiverId)
|
|
{
|
|
try
|
|
{
|
|
ViewData["UserCulture"] = _cultures[UserLanguage];
|
|
|
|
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync(
|
|
Success: er => View().WithData("EnvelopeKey", envelopeReceiverId),
|
|
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<IActionResult> 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<EnvelopeReceiverSecretDto, IActionResult>(
|
|
SuccessAsync: async er_secret =>
|
|
{
|
|
async Task<IActionResult> SendSmsView()
|
|
{
|
|
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, envelopeReceiverId: envelopeReceiverId);
|
|
if (res.Ok)
|
|
return View("EnvelopeLocked").WithData("ViaSms", true).WithData("Expiration", res.Expiration);
|
|
else if (!res.Allowed)
|
|
return View("EnvelopeLocked").WithData("ViaSms", true).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();
|
|
}
|
|
}
|
|
|
|
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.HasPhoneNumber)
|
|
{
|
|
return await SendSmsView();
|
|
}
|
|
}
|
|
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 SendSmsView();
|
|
}
|
|
}
|
|
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<Claim> {
|
|
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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
|
|
}
|
|
} |