diff --git a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs index 87e4d3f9..54b90b4b 100644 --- a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs +++ b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs @@ -4,7 +4,7 @@ using System.Text; namespace EnvelopeGenerator.Application.DTOs.Receiver { - public record ReceiverCreateDto([EmailAddress] string EmailAddress, string? TotpSecretkey = null, DateTime? TotpExpiration = null) + public record ReceiverCreateDto([EmailAddress] string EmailAddress, string? TotpSecretkey = null) { public string Signature => sha256HexOfMail.Value; diff --git a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs index 6d1b5a19..7759885a 100644 --- a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs +++ b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs @@ -4,23 +4,21 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; using System.Text.Json.Serialization; -namespace EnvelopeGenerator.Application.DTOs.Receiver +namespace EnvelopeGenerator.Application.DTOs.Receiver; + +public record ReceiverReadDto( + int Id, + string EmailAddress, + string Signature, + DateTime AddedWhen + ) : BaseDTO(Id), IUnique { - public record ReceiverReadDto( - int Id, - string EmailAddress, - string Signature, - DateTime AddedWhen - ) : BaseDTO(Id), IUnique - { - [JsonIgnore] - public IEnumerable? EnvelopeReceivers { get; init; } + [JsonIgnore] + public IEnumerable? EnvelopeReceivers { get; init; } - public string? LastUsedName => EnvelopeReceivers?.LastOrDefault()?.Name; + public string? LastUsedName => EnvelopeReceivers?.LastOrDefault()?.Name; - public string? TotpSecretkey { get; set; } = null; + public string? TotpSecretkey { get; set; } = null; - [TemplatePlaceholder("[TFA_QR_EXPIRATION]")] - public DateTime? TotpExpiration { get; set; } = null; - }; -} \ No newline at end of file + public DateTime? TfaRegDeadline { get; set; } +}; \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs index 08ec5616..88482822 100644 --- a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs +++ b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs @@ -1,6 +1,5 @@ using DigitalData.Core.Abstractions; -namespace EnvelopeGenerator.Application.DTOs.Receiver -{ - public record ReceiverUpdateDto(int Id, string? TotpSecretkey = null, DateTime? TotpExpiration = null) : IUnique; -} \ No newline at end of file +namespace EnvelopeGenerator.Application.DTOs.Receiver; + +public record ReceiverUpdateDto(int Id, string? TotpSecretkey = null, DateTime? TfaRegDeadline = null) : IUnique; \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Extensions/DIExtensions.cs b/EnvelopeGenerator.Application/Extensions/DIExtensions.cs index 6ebadb61..f5156fdf 100644 --- a/EnvelopeGenerator.Application/Extensions/DIExtensions.cs +++ b/EnvelopeGenerator.Application/Extensions/DIExtensions.cs @@ -11,64 +11,59 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using DigitalData.Core.Client; using QRCoder; -namespace EnvelopeGenerator.Application.Extensions +namespace EnvelopeGenerator.Application.Extensions; + +public static class DIExtensions { - public static class DIExtensions + public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config) { - public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config) - { - //Inject CRUD Service and repositoriesad - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); + //Inject CRUD Service and repositoriesad + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); - //Auto mapping profiles - services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly); - services.AddAutoMapper(typeof(UserMappingProfile).Assembly); + //Auto mapping profiles + services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly); + services.AddAutoMapper(typeof(UserMappingProfile).Assembly); - services.ConfigureByTypeName(config); - services.ConfigureByTypeName(config); - services.ConfigureByTypeName(config); - services.ConfigureByTypeName(config); + services.Configure(config.GetSection(nameof(DispatcherParams))); + services.Configure(config.GetSection(nameof(MailParams))); + services.Configure(config.GetSection(nameof(AuthenticatorParams))); + services.Configure(config.GetSection(nameof(TotpSmsParams))); - services.AddHttpClientService(config.GetSection(nameof(GtxMessagingParams))); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); + services.AddHttpClientService(config.GetSection(nameof(GtxMessagingParams))); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); - return services; - } - - //TODO: move to DigitalData.Core - private static IServiceCollection ConfigureByTypeName(this IServiceCollection services, IConfiguration configuration) where TOptions : class - => services.Configure(configuration.GetSection(nameof(TOptions))); + return services; } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Extensions/DTOExtensions.cs b/EnvelopeGenerator.Application/Extensions/DTOExtensions.cs deleted file mode 100644 index f1cd28ca..00000000 --- a/EnvelopeGenerator.Application/Extensions/DTOExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using EnvelopeGenerator.Application.DTOs.Receiver; -using EnvelopeGenerator.Extensions; -using Newtonsoft.Json; - -namespace EnvelopeGenerator.Application.Extensions -{ - public static class DTOExtensions - { - public static bool IsTotpSecretExpired(this ReceiverReadDto dto, int minutesBeforeExpiration = 30) - => dto.TotpExpiration < DateTime.Now.AddMinutes(minutesBeforeExpiration * -1); - - public static bool IsTotpSecretInvalid(this ReceiverReadDto dto, int minutesBeforeExpiration = 30) - => dto.IsTotpSecretExpired(minutesBeforeExpiration) || dto.TotpSecretkey is null; - - public static bool IsTotpSecretValid(this ReceiverReadDto dto, int minutesBeforeExpiration = 30) - => !dto.IsTotpSecretInvalid(minutesBeforeExpiration); - - public static bool IsTotpValid(this ReceiverReadDto dto, string totp) => dto.TotpSecretkey is null ? throw new ArgumentNullException(nameof(dto), $"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(dto)}") : totp.IsValidTotp(dto.TotpSecretkey); - - public static bool IsTotpInvalid(this ReceiverReadDto dto, string totp) => !dto.IsTotpValid(totp: totp); - } -} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx index 6a951a19..ca8a6a7d 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx @@ -163,10 +163,10 @@ Bitte überprüfen Sie die Standortinformationen. Wenn sie falsch sind, korrigieren Sie diese bitte. - Wir haben Ihnen gerade den Zugriffscode an die hinterlegte Email Adresse gesendet. Dies kann evtl. einige Minuten dauern. + Wir senden Ihnen nun einen Zugriffscode an Ihre hinterlegte Email-Adresse. Dies kann evtl. einige Minuten dauern! - Ihr QR-Code ist bis {0} gültig. + Bitte geben Sie den in Ihrer Authenticator-App angegebenen TOTP-Code ein. Wir haben den QR-Code an Ihre E-Mail-Adresse gesendet. Ihr QR-Code ist bis {0} gültig. Sie können ihn für alle Umschläge verwenden, die Sie an diese E-Mail-Adresse erhalten. @@ -184,7 +184,7 @@ SMS-Code - Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender bitten, Ihnen den Code auf anderem Wege zukommen zu lassen. + Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender <a class="mail-link" href="mailto:{0}?subject={1}&body={2}" target="_blank">{0}</a> bitten, Ihnen den Code auf anderem Wege zukommen zu lassen. Der neue QR-Code wird nur einmal für einen bestimmten Zeitraum gesendet und nach dem Scannen in Ihrer Authenticator-App gespeichert. Er kann für alle Umschläge verwendet werden, die an dieselbe E-Mail-Adresse gesendet werden, bis er abläuft. Wenn Sie die QR-Code-Mail nicht erhalten oder sie sowohl aus der Mail als auch aus authenticator löschen, kontaktieren Sie bitte den Absender. diff --git a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx index 4ae7197e..c2a8658c 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx @@ -163,10 +163,10 @@ Please review the location information. If it is incorrect, kindly make the necessary corrections. - We have just sent you the access code to the email address you provided. This may take a few minutes. + We will now send you an access code to your registered e-mail address. This may take a few minutes! - Your QR code is valid until {0}. + Please enter the TOTP provided in your Authenticator app. We have sent the QR code to your e-mail address. Your QR code is valid until {0}. You can use it for all envelopes received at this email address. @@ -184,7 +184,7 @@ SMS Code - Please check your email inbox including your spam folder. Furthermore, you can also ask the sender to send the code by other means. + Please check your email inbox including the spam folder. You can also ask the sender <a class="mail-link" href="mailto:{0}?subject={1}&body={2}" target="_blank">{0}</a> to send you the code by other means. The new QR code is sent only once for a given period and is saved in your authenticator app once scanned. It can be used for all envelopes received at the same email address until it expires. If you do not receive the QR code mail or delete it both from the mail and from authenticator, please contact the sender. diff --git a/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs b/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs index afd313db..a263aeba 100644 --- a/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs +++ b/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs @@ -164,14 +164,11 @@ namespace EnvelopeGenerator.Application.Services throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver information is missing. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}"); if (dto.Receiver.TotpSecretkey is null) throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver.TotpSecretKey is null. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}"); - if (dto.Receiver.TotpExpiration is null) - throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver.TotpExpiration is null. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}"); var totp_qr_64 = _authenticator.GenerateTotpQrCode(userEmail: dto.Receiver.EmailAddress, secretKey: dto.Receiver.TotpSecretkey).ToBase64String(); return SendAsync(dto, EmailTemplateType.TotpSecret, new() { {"[TFA_QR_CODE]", totp_qr_64 }, - {"[TFA_EXPIRATION]", dto.Receiver.TotpExpiration } }); } } diff --git a/EnvelopeGenerator.Application/Services/EnvelopeSmsHandler.cs b/EnvelopeGenerator.Application/Services/EnvelopeSmsHandler.cs index dfea09e6..36eef84f 100644 --- a/EnvelopeGenerator.Application/Services/EnvelopeSmsHandler.cs +++ b/EnvelopeGenerator.Application/Services/EnvelopeSmsHandler.cs @@ -37,13 +37,14 @@ public class EnvelopeSmsHandler : IEnvelopeSmsHandler var key = string.Format(_totpSmsParams.Expiration.CacheKeyFormat, er_secret.EnvelopeId, er_secret.ReceiverId); var expiration = await _dCache.GetDateTimeAsync(key, cToken); - if(expiration is DateTime expirationDateTime && expirationDateTime < DateTime.Now) + if(expiration is DateTime expirationDateTime && expirationDateTime >= DateTime.Now) return (null, expirationDateTime); else { var new_expiration = DateTime.Now.AddSeconds(_totpSmsParams.TotpStep); var totp = _authenticator.GenerateTotp(er_secret.Receiver!.TotpSecretkey!, _totpSmsParams.TotpStep); var msg = string.Format(_totpSmsParams.Format, totp, new_expiration.ToString(_totpSmsParams.Expiration.Format, _totpSmsParams.Expiration.CultureInfo)); + await _dCache.SetDateTimeAsync(key, new_expiration, cToken: cToken); return (await _sender.SendSmsAsync(er_secret.PhoneNumber!, msg), new_expiration); } } diff --git a/EnvelopeGenerator.Common/Constants.vb b/EnvelopeGenerator.Common/Constants.vb index 748b4723..4b3d7a33 100644 --- a/EnvelopeGenerator.Common/Constants.vb +++ b/EnvelopeGenerator.Common/Constants.vb @@ -112,6 +112,13 @@ End Enum #End Region +#Region "Role" + Public NotInheritable Class ReceiverRole + Public Const PreAuth As String = "PreAuth" + Public Const FullyAuth As String = "FullyAuth" + End Class +#End Region + #Region "Constants" Public Const DATABASE = "DATABASE" diff --git a/EnvelopeGenerator.Domain/Entities/Receiver.cs b/EnvelopeGenerator.Domain/Entities/Receiver.cs index da0928f8..0e757cf9 100644 --- a/EnvelopeGenerator.Domain/Entities/Receiver.cs +++ b/EnvelopeGenerator.Domain/Entities/Receiver.cs @@ -2,34 +2,33 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace EnvelopeGenerator.Domain.Entities +namespace EnvelopeGenerator.Domain.Entities; + +[Table("TBSIG_RECEIVER", Schema = "dbo")] +public class Receiver : IUnique { - [Table("TBSIG_RECEIVER", Schema = "dbo")] - public class Receiver : IUnique - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - [Column("GUID")] - public int Id { get; set; } - - [Required, EmailAddress] - [Column("EMAIL_ADDRESS", TypeName = "nvarchar(128)")] - public required string EmailAddress { get; set; } + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Column("GUID")] + public int Id { get; set; } + + [Required, EmailAddress] + [Column("EMAIL_ADDRESS", TypeName = "nvarchar(128)")] + public required string EmailAddress { get; set; } - [Required] - [Column("SIGNATURE", TypeName = "nvarchar(64)")] - public required string Signature { get; set; } + [Required] + [Column("SIGNATURE", TypeName = "nvarchar(64)")] + public required string Signature { get; set; } - [Required] - [Column("ADDED_WHEN", TypeName = "datetime")] - public DateTime AddedWhen { get; set; } + [Required] + [Column("ADDED_WHEN", TypeName = "datetime")] + public DateTime AddedWhen { get; set; } - [Column("TOTP_SECRET_KEY", TypeName = "nvarchar(MAX)")] - public string? TotpSecretkey { get; set; } + [Column("TOTP_SECRET_KEY", TypeName = "nvarchar(MAX)")] + public string? TotpSecretkey { get; set; } - [Column("TOTP_EXPIRATION", TypeName = "datetime")] - public DateTime? TotpExpiration { get; set; } + [Column("TFA_REG_DEADLINE", TypeName = "datetime")] + public DateTime? TfaRegDeadline { get; set; } - public IEnumerable? EnvelopeReceivers { get; init; } - } + public IEnumerable? EnvelopeReceivers { get; init; } } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Controllers/ControllerBaseExtensions.cs b/EnvelopeGenerator.Web/Controllers/ControllerBaseExtensions.cs index 0ad1e9f0..ce6cd783 100644 --- a/EnvelopeGenerator.Web/Controllers/ControllerBaseExtensions.cs +++ b/EnvelopeGenerator.Web/Controllers/ControllerBaseExtensions.cs @@ -1,4 +1,7 @@ -using EnvelopeGenerator.Web.Models; +using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; +using EnvelopeGenerator.Web.Models; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; @@ -6,6 +9,7 @@ namespace EnvelopeGenerator.Web.Controllers { public static class ControllerBaseExtensions { + #region Auth public static string? GetClaimValue(this ControllerBase controller, string claimType) => controller.User.FindFirstValue(claimType); public static string? GetAuthEnvelopeUuid(this ControllerBase controller) => controller.User.FindFirstValue(ClaimTypes.NameIdentifier); @@ -23,7 +27,35 @@ namespace EnvelopeGenerator.Web.Controllers var env_id_str = controller.User.FindFirstValue(EnvelopeClaimTypes.Id); return int.TryParse(env_id_str, out int env_id) ? env_id : null; } + + public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto er, string receiverRole) + { + var claims = new List { + new(ClaimTypes.NameIdentifier, er.Envelope!.Uuid), + new(ClaimTypes.Hash, er.Receiver!.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()), + new(ClaimTypes.Role, receiverRole) + }; + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + + var authProperties = new AuthenticationProperties + { + AllowRefresh = false, + IsPersistent = false + }; + + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity), + authProperties); + } + #endregion + + #region View error //TODO: integrate localizer for ready-to-use views public static ViewResult ViewError(this Controller controller, ErrorViewModel errorViewModel) => controller.View("_Error", errorViewModel); @@ -61,5 +93,6 @@ namespace EnvelopeGenerator.Web.Controllers Subtitle = "Ein unerwarteter Fehler ist aufgetreten", Body = "Bitte kontaktieren Sie das IT-Team." }); - } + #endregion + } } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Controllers/DocumentController.cs b/EnvelopeGenerator.Web/Controllers/DocumentController.cs index 09b10345..4283997e 100644 --- a/EnvelopeGenerator.Web/Controllers/DocumentController.cs +++ b/EnvelopeGenerator.Web/Controllers/DocumentController.cs @@ -3,12 +3,12 @@ using EnvelopeGenerator.Common; using EnvelopeGenerator.Web.Services; using EnvelopeGenerator.Application.Contracts; using Microsoft.AspNetCore.Authorization; -using EnvelopeGenerator.Application; using EnvelopeGenerator.Extensions; +using static EnvelopeGenerator.Common.Constants; namespace EnvelopeGenerator.Web.Controllers { - [Authorize] + [Authorize(Roles = ReceiverRole.FullyAuth)] [Route("api/[controller]")] public class DocumentController : BaseController { @@ -48,7 +48,7 @@ namespace EnvelopeGenerator.Web.Controllers } } - [Authorize] + [Authorize(Roles = ReceiverRole.FullyAuth)] [HttpPost("{envelopeKey}")] public async Task Open(string envelopeKey) { diff --git a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs index 9af9e4eb..671ec61b 100644 --- a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs @@ -10,7 +10,7 @@ using EnvelopeGenerator.Extensions; namespace EnvelopeGenerator.Web.Controllers { - [Authorize] + [Authorize(Roles = ReceiverRole.FullyAuth)] [ApiController] [Route("api/[controller]")] public class EnvelopeController : BaseController @@ -64,7 +64,7 @@ namespace EnvelopeGenerator.Web.Controllers } } - [Authorize] + [Authorize(Roles = ReceiverRole.FullyAuth)] [HttpPost("{envelopeKey}")] public async Task Update(string envelopeKey, int index) { @@ -110,7 +110,7 @@ namespace EnvelopeGenerator.Web.Controllers } } - [Authorize] + [Authorize(Roles = ReceiverRole.FullyAuth)] [HttpPost("reject")] public async Task Reject([FromBody] string? reason = null) { diff --git a/EnvelopeGenerator.Web/Controllers/HomeController.cs b/EnvelopeGenerator.Web/Controllers/HomeController.cs index fc4c52b8..f6128743 100644 --- a/EnvelopeGenerator.Web/Controllers/HomeController.cs +++ b/EnvelopeGenerator.Web/Controllers/HomeController.cs @@ -19,48 +19,60 @@ using Ganss.Xss; using Newtonsoft.Json; using EnvelopeGenerator.Application.DTOs; using DigitalData.Core.Client; -using EnvelopeGenerator.Application.Extensions; +using OtpNet; -namespace EnvelopeGenerator.Web.Controllers +namespace EnvelopeGenerator.Web.Controllers; + +public class HomeController : ViewControllerBase { - 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 IAuthenticator _authenticator; - private readonly IReceiverService _rcvService; - private readonly IEnvelopeSmsHandler _envSmsHandler; + private readonly EnvelopeOldService envelopeOldService; + private readonly IEnvelopeReceiverService _envRcvService; + private readonly IEnvelopeHistoryService _historyService; + private readonly IConfiguration _configuration; + private readonly IEnvelopeMailService _mailService; + private readonly IEnvelopeReceiverReadOnlyService _readOnlyService; + private readonly IAuthenticator _authenticator; + private readonly IReceiverService _rcvService; + private readonly IEnvelopeSmsHandler _envSmsHandler; - public HomeController(EnvelopeOldService envelopeOldService, ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IAuthenticator authenticator, IReceiverService receiverService, IEnvelopeSmsHandler envelopeSmsService) + public HomeController(EnvelopeOldService envelopeOldService, ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IAuthenticator authenticator, IReceiverService receiverService, IEnvelopeSmsHandler envelopeSmsService) : base(logger, sanitizer, cultures, localizer) + { + this.envelopeOldService = envelopeOldService; + _envRcvService = envelopeReceiverService; + _historyService = historyService; + _configuration = configuration; + _mailService = envelopeMailService; + _readOnlyService = readOnlyService; + _authenticator = authenticator; + _rcvService = receiverService; + _envSmsHandler = envelopeSmsService; + } + + [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) { - this.envelopeOldService = envelopeOldService; - _envRcvService = envelopeReceiverService; - _historyService = historyService; - _localizer = localizer; - _configuration = configuration; - _sanitizer = sanitizer; - _cultures = cultures; - _mailService = envelopeMailService; - _logger = logger; - _readOnlyService = readOnlyService; - _authenticator = authenticator; - _rcvService = receiverService; - _envSmsHandler = envelopeSmsService; + UserLanguage = _cultures.Default.Language; + return Redirect($"{Request.Headers["Referer"]}?culture={_cultures.Default.Language}"); } - [HttpGet("/")] - public IActionResult Main([FromQuery] string? culture = null) + 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) { @@ -68,501 +80,489 @@ namespace EnvelopeGenerator.Web.Controllers return Redirect($"{Request.Headers["Referer"]}?culture={_cultures.Default.Language}"); } - ViewData["UserCulture"] = _cultures[UserLanguage]; + envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId); - 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) - { - var (smsRes, expiration) = await _envSmsHandler.SendTotpAsync(er_secret); - - if (smsRes is not null && 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 = _authenticator.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 (_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey)) + if (!envelopeReceiverId.TryDecode(out var decoded)) { Response.StatusCode = StatusCodes.Status401Unauthorized; - ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value; - return await TFAViewAsync(viaSms: true, er_secret, envelopeReceiverId); + return this.ViewDocumentNotFound(); } - return null; - } + if(decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly) + return Redirect($"{envelopeReceiverId}/ReadOnly"); - [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); - } + ViewData["EnvelopeKey"] = 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( + return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync( SuccessAsync: async er => { - var envelopeKey = (er.Envelope!.Uuid, er.Receiver!.Signature).EncodeEnvelopeReceiverId(); + EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId); - 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) + bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress); + if (!accessCodeAlreadyRequested) { - _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); + 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(); + } } - 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(); - } + return Redirect($"{envelopeReceiverId}/Locked"); }, Fail: (messages, notices) => { _logger.LogNotice(notices); return this.ViewEnvelopeNotFound(); }); - } - catch (Exception ex) + } + 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) + .WithData("SenderEmail", er.Envelope.User!.Email) + .WithData("EnvelopeTitle", er.Envelope.Title), + 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) + { + 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) { - _logger.LogError(ex, "An unexpected error occurred while displaying a read-only envelope. Read-only key is {readOnlyKey}. {message}", readOnlyKey, ex.Message); + 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); + } + } + + [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("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[WebKey.WrongAccessCode].Value); } - [Authorize] - [HttpGet("IsAuthenticated")] - public IActionResult IsAuthenticated() + 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 envelopeUuid = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - var receiverSignature = User.FindFirst(ClaimTypes.Hash)?.Value; - return Ok(new { EnvelopeUuid = envelopeUuid, ReceiverSignature = receiverSignature }); + 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); } - [HttpPost("lang/{language}")] - public IActionResult SetLanguage([FromRoute] string language) + 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)) { - 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); - } + Response.StatusCode = StatusCodes.Status401Unauthorized; + ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value; + return await TFAViewAsync(viaSms: true, er_secret, envelopeReceiverId); } - [HttpGet("lang")] - public IActionResult GetLanguages() => Ok(_cultures.Languages); + return null; + } - private string? UserLanguage + [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)) { - get + 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]; + ViewData["EnvelopeKey"] = envelopeReceiverId; + + envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId); + (string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId(); + + if (uuid is null || signature is null) { - var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName]; - - if (string.IsNullOrEmpty(cookieValue)) - return null; - - var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0]; - return culture?.Value ?? null; + _logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[WebKey.WrongEnvelopeReceiverId]); + return Unauthorized(); } - set + + _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) { - if(value is null) - Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName); + _logger.LogNotice(er_secret_res.Notices); + return this.ViewEnvelopeNotFound(); + } + var er_secret = er_secret_res.Data; + + 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[WebKey.WrongAccessCode].Value); + } + + //continue the process without important data to minimize security errors. + EnvelopeReceiverDto er = er_secret; + + //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(); + } + + 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(); + } + } + + [Authorize(Roles = ReceiverRole.FullyAuth)] + [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(Roles = ReceiverRole.FullyAuth)] + [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 { - 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); + _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(Roles = ReceiverRole.FullyAuth)] + [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(); } + + public IActionResult Error404() => this.ViewError404(); } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Controllers/ReadOnlyController.cs b/EnvelopeGenerator.Web/Controllers/ReadOnlyController.cs index 6b910d71..c541b0d1 100644 --- a/EnvelopeGenerator.Web/Controllers/ReadOnlyController.cs +++ b/EnvelopeGenerator.Web/Controllers/ReadOnlyController.cs @@ -4,6 +4,7 @@ using EnvelopeGenerator.Application.DTOs.EnvelopeReceiverReadOnly; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; +using static EnvelopeGenerator.Common.Constants; namespace EnvelopeGenerator.Web.Controllers { @@ -28,7 +29,7 @@ namespace EnvelopeGenerator.Web.Controllers } [HttpPost] - [Authorize] + [Authorize(Roles = ReceiverRole.FullyAuth)] public async Task CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto) { try diff --git a/EnvelopeGenerator.Web/Controllers/TFARegController.cs b/EnvelopeGenerator.Web/Controllers/TFARegController.cs new file mode 100644 index 00000000..04832945 --- /dev/null +++ b/EnvelopeGenerator.Web/Controllers/TFARegController.cs @@ -0,0 +1,87 @@ +using EnvelopeGenerator.Application.Contracts; +using EnvelopeGenerator.Web.Models; +using Ganss.Xss; +using Microsoft.AspNetCore.Mvc; +using EnvelopeGenerator.Extensions; +using Microsoft.Extensions.Localization; +using EnvelopeGenerator.Application.Resources; +using DigitalData.Core.DTO; +using EnvelopeGenerator.Application.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Authorization; + +namespace EnvelopeGenerator.Web.Controllers; + +//TODO: Add authorization as well as limiting the link duration (intermediate token with different role) or sign it +[Route("tfa")] +public class TFARegController : ViewControllerBase +{ + private readonly IEnvelopeReceiverService _envRcvService; + private readonly IAuthenticator _authenticator; + private readonly IReceiverService _rcvService; + private readonly TFARegParams _params; + + public TFARegController(ILogger logger, HtmlSanitizer sanitizer, Cultures cultures, IStringLocalizer localizer, IEnvelopeReceiverService erService, IAuthenticator authenticator, IReceiverService receiverService, IOptions tfaRegParamsOptions) : base(logger, sanitizer, cultures, localizer) + { + _envRcvService = erService; + _authenticator = authenticator; + _rcvService = receiverService; + _params = tfaRegParamsOptions.Value; + } + + [Authorize] + [HttpGet("{envelopeReceiverId}")] + public async Task Reg(string envelopeReceiverId) + { + try + { + 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(); + } + + 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 (!er_secret.Envelope!.TFAEnabled) + return Unauthorized(); + + var rcv = er_secret.Receiver; + + // Generate QR code as base 64 + rcv!.TotpSecretkey = _authenticator.GenerateTotpSecretKey(); + await _rcvService.UpdateAsync(rcv); + var totp_qr_64 = _authenticator.GenerateTotpQrCode(userEmail: rcv.EmailAddress, secretKey: rcv.TotpSecretkey).ToBase64String(); + + // Calculate RFA registiration deadline + if (rcv.TfaRegDeadline is null) + { + rcv.TfaRegDeadline = _params.Deadline; + await _rcvService.UpdateAsync(rcv); + } + else if (rcv.TfaRegDeadline <= DateTime.Now) + return View("_Expired"); + + ViewData["RegDeadline"] = rcv.TfaRegDeadline; + + ViewData["TotpQR64"] = totp_qr_64; + + return View(); + } + catch(Exception ex) + { + _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer[WebKey.UnexpectedError]); + return this.ViewInnerServiceError(); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Controllers/ViewControllerBase.cs b/EnvelopeGenerator.Web/Controllers/ViewControllerBase.cs new file mode 100644 index 00000000..9ff5eff8 --- /dev/null +++ b/EnvelopeGenerator.Web/Controllers/ViewControllerBase.cs @@ -0,0 +1,23 @@ +using EnvelopeGenerator.Web.Models; +using Ganss.Xss; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; +using EnvelopeGenerator.Application.Resources; + +namespace EnvelopeGenerator.Web.Controllers; + +public class ViewControllerBase : Controller +{ + protected readonly ILogger _logger; + protected readonly HtmlSanitizer _sanitizer; + protected readonly Cultures _cultures; + protected readonly IStringLocalizer _localizer; + + public ViewControllerBase(ILogger logger, HtmlSanitizer sanitizer, Cultures cultures, IStringLocalizer localizer) + { + _logger = logger; + _sanitizer = sanitizer; + _cultures = cultures; + _localizer = localizer; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj index 1c25a308..d64f6c49 100644 --- a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj +++ b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj @@ -5,7 +5,7 @@ enable enable EnvelopeGenerator.Web - 2.9.0 + 2.10.4 Digital Data GmbH Digital Data GmbH EnvelopeGenerator.Web @@ -13,8 +13,8 @@ digital data envelope generator web EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly. Assets\icon.ico - 2.9.0 - 2.9.0 + 2.10.4 + 2.10.4 Copyright © 2024 Digital Data GmbH. All rights reserved. @@ -41,6 +41,2061 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EnvelopeGenerator.Web/Models/TFARegParams.cs b/EnvelopeGenerator.Web/Models/TFARegParams.cs new file mode 100644 index 00000000..eb39a62b --- /dev/null +++ b/EnvelopeGenerator.Web/Models/TFARegParams.cs @@ -0,0 +1,17 @@ +namespace EnvelopeGenerator.Web.Models; + +/// +/// Represents the parameters for two-factor authentication (2FA) registration. +/// +public class TFARegParams +{ + /// + /// The maximum allowed time for completing the registration process. + /// + public TimeSpan TimeLimit { get; init; } = new(0, 30, 0); + + /// + /// The deadline for registration, calculated as the current time plus the . + /// + public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks); +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Program.cs b/EnvelopeGenerator.Web/Program.cs index fbf24996..35b12185 100644 --- a/EnvelopeGenerator.Web/Program.cs +++ b/EnvelopeGenerator.Web/Program.cs @@ -49,6 +49,8 @@ try // Add higher order services builder.Services.AddScoped(); + builder.ConfigureBySection(); + // Add controllers and razor views builder.Services.AddControllersWithViews(options => { diff --git a/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml b/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml index ab2eb857..008209a2 100644 --- a/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml @@ -2,20 +2,23 @@ @using Newtonsoft.Json @model Auth; @{ + //TODO: Create view model var nonce = _accessor.HttpContext?.Items["csp-nonce"] as string; var logo = _logoOpt.Value; ViewData["Title"] = _localizer[WebKey.DocProtected]; var userCulture = ViewData["UserCulture"] as Culture; string codeType = ViewData["CodeType"] is string _codeType ? _codeType : "accessCode"; - string codePropName = char.ToUpper(codeType[0]) + codeType.Substring(1); - string codeKeyName = codePropName.Replace("Code", ""); + string codeKeyName = (char.ToUpper(codeType[0]) + codeType.Substring(1)).Replace("Code", ""); bool viaSms = codeType == "smsCode"; bool viaAuthenticator = codeType == "authenticatorCode"; bool viaTFA = viaSms || viaAuthenticator; DateTime? smsExpiration = ViewData["SmsExpiration"] is DateTime _smsExpiration ? _smsExpiration : null; - DateTime? qrCodeExpiration = ViewData["QRCodeExpiration"] is DateTime _qrCodeExpiration ? _qrCodeExpiration : null; bool tfaEnabled = ViewData["TFAEnabled"] is bool _tfaEnabled && _tfaEnabled; bool hasPhoneNumber = ViewData["HasPhoneNumber"] is bool _hasPhoneNumber && _hasPhoneNumber; + var envelopeKey = ViewData["EnvelopeKey"] as string; + DateTime? tfaRegDeadline = ViewData["TfaRegDeadline"] is DateTime _deadline ? _deadline : null; + var senderEmail = ViewData["SenderEmail"] as string ?? string.Empty; + var envelopeTitle = ViewData["EnvelopeTitle"] as string ?? string.Empty; }
@@ -31,12 +34,25 @@

@_localizer[WebKey.Formats.LockedTitle.Format(codeKeyName)]

+ @if (viaAuthenticator && (tfaRegDeadline is null || tfaRegDeadline > DateTime.Now)) + {
-

@_localizer[WebKey.Formats.LockedBody.Format(codeKeyName)].Value.Format(qrCodeExpiration.ToString())

+

+ Klicken Sie auf den + + Link + + + um Ihre Authenticator-App einzurichten. +

+
+ } +
+

@_localizer[WebKey.Formats.LockedBody.Format(codeKeyName)].Value

-
+
@@ -47,21 +63,21 @@ @if (tfaEnabled) { -
+
@if(hasPhoneNumber) - { - - } - else - { - - } - -
+ { + + } + else + { + + } + +
} @if (smsExpiration is not null) { - + }
@@ -69,15 +85,15 @@
@if (ViewData["ErrorMessage"] is string errMsg) { - }
@_localizer[WebKey.Formats.LockedFooterTitle.Format(codeKeyName)] -

@_localizer[WebKey.Formats.LockedFooterBody.Format(codeKeyName)]

+

@Html.Raw(_localizer[WebKey.Formats.LockedFooterBody.Format(codeKeyName)].Value.Format(senderEmail, "Envelope - " + envelopeTitle, string.Empty))

diff --git a/EnvelopeGenerator.Web/Views/Shared/_Expired.cshtml b/EnvelopeGenerator.Web/Views/Shared/_Expired.cshtml new file mode 100644 index 00000000..bc5f6b54 --- /dev/null +++ b/EnvelopeGenerator.Web/Views/Shared/_Expired.cshtml @@ -0,0 +1,25 @@ +@{ + ViewData["Title"] = "Abgelaufen"; + var head = ViewData["Head"] as string ?? "Abgelaufen!"; + var body = ViewData["Body"] as string ?? "Die Gültigkeitsdauer der Verbindung ist abgelaufen."; +} +
+
+
+ + + + + + + + + + +
+

@head

+
+
+

@body

+
+
\ No newline at end of file diff --git a/EnvelopeGenerator.Web/Views/Shared/_Layout.cshtml b/EnvelopeGenerator.Web/Views/Shared/_Layout.cshtml index 62be76b2..97f17d75 100644 --- a/EnvelopeGenerator.Web/Views/Shared/_Layout.cshtml +++ b/EnvelopeGenerator.Web/Views/Shared/_Layout.cshtml @@ -26,6 +26,7 @@ +