Compare commits

...

14 Commits

Author SHA1 Message Date
Developer 02
a6468c2ff1 feat(HomeController): Funktionalität zur Überprüfung des SMS-Codes hinzugefügt 2024-11-30 04:23:24 +01:00
Developer 02
40a21a0b89 feat(EnvelopeReceiverCache): zum Abrufen und Setzen von Caches über Envelope Receiver unter Verwendung von Standard-Schlüsselwörtern als Schnittstellenimplementierung erstellt.
- Erstellte Optionen.
 - Zu DI hinzugefügt.
2024-11-30 03:46:40 +01:00
Developer 02
fa44b82493 feat(EnvelopeLocked): Timer mit CSS-Konfiguration und Javascript-Ereignis hinzugefügt.
- Ablauf über Home-Controller-Ansichtsdaten hinzugefügt
2024-11-30 01:56:02 +01:00
Developer 02
cdec5485c6 feat(GtxMessagingService): Zwischenspeicherung für SMS-Code und Ablauf des SMS-Codes mittels Envelope-Receiver-ID hinzugefügt
- Erweiterungsmethode für Zeitcaching hinzugefügt.
2024-11-29 16:25:20 +01:00
Developer 02
2a963a1861 feat(Web): Verteilter Sql Server-Cache hinzugefügt.
- Bat-Datei erstellt, um Tabelle für Cache zu erstellen.
 - Sql-Datei zum Erstellen einer Tabelle für den Cache erstellt
2024-11-29 14:08:07 +01:00
Developer 02
9d1a2e7254 refactor(HomeController): SMS-Code zum Senden hinzugefügt 2024-11-29 12:05:07 +01:00
Developer 02
b779ef6f0b feat(GtxMessagingService): Konfigurierte Codelänge über ioptions.
- Standardmäßig ist sie 5
2024-11-29 11:16:08 +01:00
Developer 02
0c81a86610 feat(GtxMessagingService): SendSmsCodeAsync mit Basisfunktionalität als Schnittstellenimplementierung hinzugefügt 2024-11-29 11:13:59 +01:00
Developer 02
b11f32bd3c feat: CodeGenerator-Service mit Konfigurationsunterstützung implementiert
- CodeGenerator-Service erstellt, der zufällige Codes basierend auf einem konfigurierbaren Zeichensatz generiert.
- IOptions<CodeGeneratorConfig> für DI-Injektion der Konfigurationseinstellungen integriert.
- Lazy-Initialisierung für statische Instanz des CodeGenerators hinzugefügt.
- Validierung hinzugefügt, um sicherzustellen, dass die Code-Länge größer als null ist.
- Geplante zukünftige Verbesserung: Random als Singleton injizieren, um die Multithreading-Performance zu verbessern.
2024-11-29 11:08:01 +01:00
Developer 02
b8d9963fac refactor(HomeController): ReadWithSecretByUuidSignatureAsync implementiert, um alle Informationen in einer einzigen Sql-Transaktion zu erhalten.
- Methode hinzugefügt, um geheimes dto in dto zu konvertieren
2024-11-29 10:22:11 +01:00
Developer 02
e77532ebfd feat(EnvelopeReceiverService): ReadWithSecretByUuidSignatureAsync zum Lesen mit Zugangscode und Telefonnummer hinzugefügt 2024-11-29 10:11:33 +01:00
Developer 02
ec37518245 feat(EnvelopeReceiverSecretDto): Erstellt als Erbe von EnvelopeReceiverDto, hinzugefügt AccessCode und PhoneNumber. 2024-11-29 10:07:09 +01:00
Developer 02
a1618fc8d0 refactor(HomeController): EnvelopeReceiverSecretDto zur Vereinfachung entfernt und direkt String verwendet 2024-11-29 10:01:28 +01:00
Developer 02
6b65fc28fd refactor(HomeController): log message format more appropriately written 2024-11-29 09:29:27 +01:00
30 changed files with 487 additions and 51 deletions

View File

@@ -0,0 +1,7 @@
namespace EnvelopeGenerator.Application.Configurations
{
public class CodeGeneratorConfig
{
public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789";
}
}

View File

@@ -0,0 +1,19 @@
namespace EnvelopeGenerator.Application.Configurations
{
public class EnvelopeReceiverCacheParams
{
/// <summary>
/// Gets the cache key format for SMS codes.
/// The placeholder {0} represents the envelopeReceiverId.
/// </summary>
public string CodeCacheKeyFormat { get; init; } = "sms-code-{0}";
/// <summary>
/// Gets the cache expiration key format for SMS codes.
/// The placeholder {0} represents the envelopeReceiverId.
/// </summary>
public string CodeExpirationCacheKeyFormat { get; init; } = "sms-code-expiration-{0}";
public TimeSpan CodeCacheValidityPeriod { get; init; } = new(0, 5, 0);
}
}

View File

@@ -1,4 +1,5 @@
using DigitalData.Core.Abstractions.Client;
using Microsoft.Extensions.Caching.Distributed;
namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
{
@@ -18,5 +19,7 @@ namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
public string RecipientQueryParamName { get; init; } = "to";
public string MessageQueryParamName { get; init; } = "text";
public int CodeLength { get; init; } = 5;
}
}

View File

@@ -0,0 +1,7 @@
namespace EnvelopeGenerator.Application.Contracts
{
public interface ICodeGenerator
{
string GenerateCode(int length);
}
}

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.Application.Contracts
{
public interface IEnvelopeReceiverCache
{
Task<string?> GetSmsCodeAsync(string envelopeReceiverId);
/// <summary>
/// Asynchronously stores an SMS verification code in the cache and returns the expiration date of the code.
/// </summary>
/// <param name="envelopeReceiverId">The unique identifier for the recipient of the envelope to associate with the SMS code.</param>
/// <param name="code">The SMS verification code to be stored.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the expiration date and time of the stored SMS code.</returns>
Task<DateTime> SetSmsCodeAsync(string envelopeReceiverId, string code);
Task<DateTime?> GetSmsCodeExpirationAsync(string envelopeReceiverId);
}
}

View File

@@ -11,12 +11,14 @@ namespace EnvelopeGenerator.Application.Contracts
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false);
Task<DataResult<IEnumerable<EnvelopeReceiverSecretDto>>> ReadSecretByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true);
Task<DataResult<IEnumerable<string?>>> ReadAccessCodeByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true);
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true);
Task<DataResult<EnvelopeReceiverDto>> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true);
Task<DataResult<EnvelopeReceiverSecretDto>> ReadWithSecretByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true);
Task<DataResult<EnvelopeReceiverDto>> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true);
Task<DataResult<string>> ReadAccessCodeByIdAsync(int envelopeId, int receiverId);

View File

@@ -4,8 +4,10 @@ namespace EnvelopeGenerator.Application.Contracts
{
public interface IMessagingService
{
public Task<SmsResponse> SendSmsAsync(string recipient, string message);
string ServiceProvider { get; }
Task<SmsResponse> SendSmsAsync(string recipient, string message);
Task<SmsResponse> SendSmsCodeAsync(string recipient, string envelopeReceiverId);
}
}

View File

@@ -1,4 +1,9 @@
namespace EnvelopeGenerator.Application.DTOs.EnvelopeReceiver
{
public record EnvelopeReceiverSecretDto(string? AccessCode) : EnvelopeReceiverDto;
public record EnvelopeReceiverSecretDto() : EnvelopeReceiverDto()
{
public string? AccessCode { get; init; }
public string? PhoneNumber { get; init; }
}
}

View File

@@ -4,6 +4,8 @@
{
public required bool Ok { get; init; }
public DateTime? Expiration { get; set; }
public DateTime? AllowedAt { get; set; }
public TimeSpan AllowedAfter => Allowed ? TimeSpan.Zero : AllowedAt!.Value - DateTime.Now;

View File

@@ -0,0 +1,43 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EnvelopeGenerator.Application.Extensions
{
public static class CacheExtensions
{
public static Task SetLongAsync(this IDistributedCache cache, string key, long value, DistributedCacheEntryOptions? options = null)
=> options is null
? cache.SetAsync(key, BitConverter.GetBytes(value))
: cache.SetAsync(key, BitConverter.GetBytes(value), options: options);
public static async Task<long?> GetLongAsync(this IDistributedCache cache, string key)
{
var value = await cache.GetAsync(key);
return value is null ? null : BitConverter.ToInt64(value, 0);
}
public static Task SetDateTimeAsync(this IDistributedCache cache, string key, DateTime value, DistributedCacheEntryOptions? options = null)
=> cache.SetLongAsync(key: key, value: value.Ticks, options: options);
public static async Task<DateTime?> GetDateTimeAsync(this IDistributedCache cache, string key)
{
var value = await cache.GetAsync(key);
return value is null ? null : new(BitConverter.ToInt64(value, 0));
}
public static Task SetTimeSpanAsync(this IDistributedCache cache, string key, TimeSpan value, DistributedCacheEntryOptions? options = null)
=> cache.SetLongAsync(key: key, value: value.Ticks, options: options);
public static async Task<TimeSpan?> GetTimeSpanAsync(this IDistributedCache cache, string key)
{
var value = await cache.GetAsync(key);
return value is null ? null : new(BitConverter.ToInt64(value, 0));
}
}
}

View File

@@ -11,11 +11,11 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using DigitalData.Core.Client;
using EnvelopeGenerator.Application.Configurations.GtxMessaging;
namespace EnvelopeGenerator.Application
namespace EnvelopeGenerator.Application.Extensions
{
public static class DIExtensions
{
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfigurationSection dispatcherConfigSection, IConfigurationSection mailConfigSection, IConfigurationSection smsConfigSection)
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfigurationSection dispatcherConfigSection, IConfigurationSection mailConfigSection, IConfigurationSection smsConfigSection, IConfigurationSection codeGeneratorConfigSection, IConfigurationSection envelopeReceiverCacheParamsSection)
{
//Inject CRUD Service and repositoriesad
services.TryAddScoped<IConfigRepository, ConfigRepository>();
@@ -55,9 +55,13 @@ namespace EnvelopeGenerator.Application
services.Configure<DispatcherConfig>(dispatcherConfigSection);
services.Configure<MailConfig>(mailConfigSection);
services.Configure<CodeGeneratorConfig>(codeGeneratorConfigSection);
services.Configure<EnvelopeReceiverCacheParams>(envelopeReceiverCacheParamsSection);
services.AddHttpClientService<SmsParams>(smsConfigSection);
services.TryAddSingleton<IMessagingService, GtxMessagingService>();
services.TryAddSingleton<ICodeGenerator, CodeGenerator>();
services.TryAddSingleton<IEnvelopeReceiverCache, EnvelopeReceiverCache>();
return services;
}
@@ -65,6 +69,8 @@ namespace EnvelopeGenerator.Application
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config) => services.AddEnvelopeGenerator(
dispatcherConfigSection: config.GetSection("DispatcherConfig"),
mailConfigSection: config.GetSection("MailConfig"),
smsConfigSection: config.GetSection("SmsConfig"));
smsConfigSection: config.GetSection("SmsConfig"),
codeGeneratorConfigSection: config.GetSection("CodeGeneratorConfig"),
envelopeReceiverCacheParamsSection: config.GetSection("EnvelopeReceiverCacheParams"));
}
}

View File

@@ -1,6 +1,7 @@
using EnvelopeGenerator.Domain.HttpResponse;
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
using EnvelopeGenerator.Domain.HttpResponse;
namespace EnvelopeGenerator.Application
namespace EnvelopeGenerator.Application.Extensions
{
public static class MappingExtensions
{

View File

@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Application.DTOs.Messaging;
using EnvelopeGenerator.Application.DTOs.Receiver;
using EnvelopeGenerator.Application.Extensions;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.HttpResponse;

View File

@@ -0,0 +1,37 @@
using EnvelopeGenerator.Application.Configurations;
using EnvelopeGenerator.Application.Contracts;
using Microsoft.Extensions.Options;
using System.Text;
namespace EnvelopeGenerator.Application.Services
{
public class CodeGenerator : ICodeGenerator
{
public static Lazy<CodeGenerator> LazyStatic => new(() => new CodeGenerator(Options.Create<CodeGeneratorConfig>(new())));
public static CodeGenerator Static => LazyStatic.Value;
private readonly string _charPool;
public CodeGenerator(IOptions<CodeGeneratorConfig> options)
{
_charPool = options.Value.CharPool;
}
public string GenerateCode(int length)
{
//TODO: Inject Random as a singleton to support multithreading to improve performance.
Random random = new();
if (length <= 0)
throw new ArgumentException("Password length must be greater than 0.");
var passwordBuilder = new StringBuilder(length);
for (int i = 0; i < length; i++)
passwordBuilder.Append(_charPool[random.Next(_charPool.Length)]);
return passwordBuilder.ToString();
}
}
}

View File

@@ -0,0 +1,50 @@
using AngleSharp.Dom;
using EnvelopeGenerator.Application.Configurations;
using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.Extensions;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Services
{
public class EnvelopeReceiverCache : IEnvelopeReceiverCache
{
private readonly EnvelopeReceiverCacheParams _cacheParams;
private readonly DistributedCacheEntryOptions _codeCacheOptions;
private readonly IDistributedCache _cache;
public EnvelopeReceiverCache(IOptions<EnvelopeReceiverCacheParams> cacheParamOptions, IDistributedCache cache)
{
_cacheParams = cacheParamOptions.Value;
_codeCacheOptions = new() { AbsoluteExpirationRelativeToNow = cacheParamOptions.Value.CodeCacheValidityPeriod };
_cache = cache;
}
public async Task<string?> GetSmsCodeAsync(string envelopeReceiverId)
{
var code_key = string.Format(_cacheParams.CodeCacheKeyFormat, envelopeReceiverId);
return await _cache.GetStringAsync(code_key);
}
public async Task<DateTime> SetSmsCodeAsync(string envelopeReceiverId, string code)
{
// set key
var code_key = string.Format(_cacheParams.CodeCacheKeyFormat, envelopeReceiverId);
await _cache.SetStringAsync(code_key, code, _codeCacheOptions);
// set expiration
var code_expiration_key = string.Format(_cacheParams.CodeExpirationCacheKeyFormat, envelopeReceiverId);
var expiration = DateTime.Now + _cacheParams.CodeCacheValidityPeriod;
await _cache.SetDateTimeAsync(code_expiration_key, expiration, _codeCacheOptions);
return expiration;
}
public async Task<DateTime?> GetSmsCodeExpirationAsync(string envelopeReceiverId)
{
var code_expiration_key = string.Format(_cacheParams.CodeExpirationCacheKeyFormat, envelopeReceiverId);
return await _cache.GetDateTimeAsync(code_expiration_key);
}
}
}

View File

@@ -38,10 +38,10 @@ namespace EnvelopeGenerator.Application.Services
return Result.Success(_mapper.Map<IEnumerable<EnvelopeReceiverDto>>(env_rcvs));
}
public async Task<DataResult<IEnumerable<EnvelopeReceiverSecretDto>>> ReadSecretByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true)
public async Task<DataResult<IEnumerable<string?>>> ReadAccessCodeByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true)
{
var env_rcvs = await _repository.ReadByUuidAsync(uuid: uuid, withEnvelope: withEnvelope, withReceiver: withReceiver);
return Result.Success(_mapper.Map<IEnumerable<EnvelopeReceiverSecretDto>>(env_rcvs));
return Result.Success(env_rcvs.Select(er => er.AccessCode));
}
public async Task<DataResult<EnvelopeReceiverDto>> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true)
@@ -54,6 +54,16 @@ namespace EnvelopeGenerator.Application.Services
return Result.Success(_mapper.Map<EnvelopeReceiverDto>(env_rcv));
}
public async Task<DataResult<EnvelopeReceiverSecretDto>> ReadWithSecretByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true)
{
var env_rcv = await _repository.ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver);
if (env_rcv is null)
return Result.Fail<EnvelopeReceiverSecretDto>()
.Message(Key.EnvelopeReceiverNotFound);
return Result.Success(_mapper.Map<EnvelopeReceiverSecretDto>(env_rcv));
}
public async Task<DataResult<EnvelopeReceiverDto>> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true)
{
(string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();

View File

@@ -4,7 +4,9 @@ using DigitalData.Core.Client;
using EnvelopeGenerator.Application.Configurations.GtxMessaging;
using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs.Messaging;
using EnvelopeGenerator.Application.Extensions;
using EnvelopeGenerator.Domain.HttpResponse;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Services
@@ -17,12 +19,20 @@ namespace EnvelopeGenerator.Application.Services
private readonly IMapper _mapper;
public GtxMessagingService(IHttpClientService<SmsParams> smsClient, IOptions<SmsParams> smsParamsOptions, IMapper mapper)
private readonly ICodeGenerator _codeGen;
private readonly IEnvelopeReceiverCache _erCache;
public string ServiceProvider { get; }
public GtxMessagingService(IHttpClientService<SmsParams> smsClient, IOptions<SmsParams> smsParamsOptions, IMapper mapper, ICodeGenerator codeGenerator, IEnvelopeReceiverCache envelopeReceiverCache)
{
_smsClient = smsClient;
_smsParams = smsParamsOptions.Value;
_mapper = mapper;
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
_codeGen = codeGenerator;
_erCache = envelopeReceiverCache;
}
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
@@ -36,6 +46,25 @@ namespace EnvelopeGenerator.Application.Services
.ThenAsync(_mapper.Map<SmsResponse>);
}
public string ServiceProvider { get; }
public async Task<SmsResponse> SendSmsCodeAsync(string recipient, string envelopeReceiverId)
{
var code = await _erCache.GetSmsCodeAsync(envelopeReceiverId);
if (code is null)
{
code = _codeGen.GenerateCode(_smsParams.CodeLength);
var expiration = await _erCache.SetSmsCodeAsync(envelopeReceiverId, code);
var res = await SendSmsAsync(recipient: recipient, message: code);
res.Expiration = expiration;
return res;
}
else
{
var code_expiration = await _erCache.GetSmsCodeExpirationAsync(envelopeReceiverId);
return code_expiration is null
? new() { Ok = false }
: new() { Ok = false, AllowedAt = code_expiration };
}
}
}
}

View File

@@ -1,7 +1,7 @@
using DigitalData.Core.API;
using DigitalData.Core.Application;
using DigitalData.UserManager.Application;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Extensions;
using EnvelopeGenerator.Infrastructure;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Localization;

View File

@@ -18,6 +18,8 @@ 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
{
@@ -33,8 +35,10 @@ namespace EnvelopeGenerator.Web.Controllers
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)
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;
@@ -46,6 +50,8 @@ namespace EnvelopeGenerator.Web.Controllers
_mailService = envelopeMailService;
_logger = logger;
_readOnlyService = readOnlyService;
_msgService = messagingService;
_erCache = envelopeReceiverCache;
}
[HttpGet("/")]
@@ -165,40 +171,78 @@ namespace EnvelopeGenerator.Web.Controllers
return Unauthorized();
}
_logger.LogInformation($"Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]");
_logger.LogInformation("Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]", uuid, signature);
//check access code
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId);
var verification = await _envRcvService.VerifyAccessCodeAsync(uuid: uuid, signature: signature, accessCode: auth.AccessCode!);
if (verification.IsFailed)
{
_logger.LogNotice(verification.Notices);
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
}
return await _envRcvService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync<EnvelopeReceiverDto, IActionResult>(
SuccessAsync: async er =>
return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync<EnvelopeReceiverSecretDto, IActionResult>(
SuccessAsync: async er_secret =>
{
//check the access code verification
if (verification.IsWrong())
{
//Constants.EnvelopeStatus.AccessCodeIncorrect
await _historyService.RecordAsync(er.EnvelopeId, er.Receiver!.EmailAddress, Constants.EnvelopeStatus.AccessCodeIncorrect);
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
}
await _historyService.RecordAsync(er.EnvelopeId, er.Receiver!.EmailAddress, Constants.EnvelopeStatus.AccessCodeCorrect);
//check if the user has phone is added
if (er.HasPhoneNumber)
async Task<IActionResult> SendSmsView()
{
return View("EnvelopeLocked").WithData("ViaSms", true);
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);

View File

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
namespace EnvelopeGenerator.Web.Controllers.Test
{
[Route("api/[controller]")]
[ApiController]
public class TestCacheController : ControllerBase
{
private readonly IDistributedCache _cache;
public TestCacheController(IDistributedCache cache)
{
_cache = cache;
}
[HttpPost]
public async Task<IActionResult> SetCacheAsync(string key, string value)
{
var options = new DistributedCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
await _cache.SetStringAsync(key, value, options);
return Ok();
}
[HttpGet]
public async Task<IActionResult> GetCacheAsync(string key)
{
var value = await _cache.GetStringAsync(key);
return value is null ? BadRequest() : Ok(value);
}
}
}

View File

@@ -47,7 +47,7 @@
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.API" Version="2.0.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.0.1" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="2.0.0" />
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
@@ -57,6 +57,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.15" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
@@ -120,6 +121,12 @@
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Update="Scripts\create-sql-cache.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Scripts\create-sql-cache.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,4 +1,3 @@
using DigitalData.UserManager.Infrastructure.Repositories;
using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.Services;
using EnvelopeGenerator.Web.Services;
@@ -9,7 +8,6 @@ using NLog.Web;
using DigitalData.Core.API;
using Microsoft.AspNetCore.Authentication.Cookies;
using EnvelopeGenerator.Web.Models;
using DigitalData.Core.DTO;
using System.Text.Encodings.Web;
using Ganss.Xss;
using Microsoft.Extensions.Options;
@@ -17,6 +15,7 @@ using EnvelopeGenerator.Application;
using DigitalData.EmailProfilerDispatcher;
using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Web.Sanitizers;
using EnvelopeGenerator.Application.Extensions;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@@ -81,6 +80,12 @@ try
//AddEF Core dbcontext
var connStr = config.GetConnectionString(Key.Default) ?? throw new InvalidOperationException("There is no default connection string in appsettings.json.");
builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
builder.Services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = connStr;
options.SchemaName = "dbo";
options.TableName = "TBDD_CACHE";
});
// Add envelope generator services
builder.Services.AddEnvelopeGenerator(config);

View File

@@ -0,0 +1,2 @@
dotnet sql-cache create "CONNECTION_STRING" dbo TBDD_CACHE
pause

View File

@@ -0,0 +1,23 @@
USE [DD_ECM]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[TBDD_CACHE](
[Id] [nvarchar](449) NOT NULL,
[Value] [varbinary](max) NOT NULL,
[ExpiresAtTime] [datetimeoffset](7) NOT NULL,
[SlidingExpirationInSeconds] [bigint] NULL,
[AbsoluteExpiration] [datetimeoffset](7) NULL,
PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

View File

@@ -1,4 +1,5 @@
@using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
@using Newtonsoft.Json
@{
var nonce = _accessor.HttpContext?.Items["csp-nonce"] as string;
var logo = _logoOpt.Value;
@@ -6,6 +7,7 @@
var userCulture = ViewData["UserCulture"] as Culture;
bool viaSms = ViewData["ViaSms"] is bool _viaSms && _viaSms;
var accessCodeName = viaSms ? "smsCode" : "accessCode";
DateTime? expiration = ViewData["Expiration"] is DateTime _expiration ? _expiration : null;
}
<div class="page container py-4 px-4">
<header class="text-center">
@@ -35,6 +37,10 @@
login
</span>
</button>
@if (expiration is not null)
{
<div id="sms-timer" class="alert alert-primary" role="alert">00:00</div>
}
</div>
</form>
</div>
@@ -52,4 +58,30 @@
<p>@_localizer[viaSms ? WebKey.LockedSmsTfaFooterBody : WebKey.LockedFooterBody]</p>
</details>
</section>
</div>
</div>
<script nonce="@nonce">
var expiration = new Date(@Html.Raw(JsonConvert.SerializeObject(expiration)));
const element = document.getElementById("sms-timer");
const interval = setInterval(function () {
var now = new Date();
var diffInMillis = expiration - now;
if (diffInMillis <= 0) {
element.textContent = "00:00";
clearInterval(interval);
return;
}
var minutes = Math.floor(diffInMillis / 1000 / 60);
var seconds = Math.floor((diffInMillis / 1000) % 60);
var formattedMinutes = minutes.toString().padStart(2, '0');
var formattedSeconds = seconds.toString().padStart(2, '0');
var remainingTime = `${formattedMinutes}:${formattedSeconds}`;
element.textContent = remainingTime;
}, 1000);
</script>

View File

@@ -135,6 +135,8 @@
"Headers": {},
"QueryParams": {
"from": "signFlow"
}
}
},
"CodeCacheValidityPeriod": "00:10:00"
},
"EnvelopeReceiverCacheParams": {}
}

View File

@@ -402,14 +402,17 @@ footer#page-footer {
.access-code-form-floating {
display: flex;
justify-content: start;
justify-content: space-between;
flex-direction: row;
align-items: center;
}
.access-code-form-floating button {
align-content: center;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin:0;
height: 100%;
}
.access-code-form-floating input {
@@ -427,6 +430,24 @@ footer#page-footer {
height: 2.5rem;
}
#sms-timer {
height: 3rem;
display: flex;
align-items: center;
font-family: 'Arial', sans-serif;
font-weight: bold;
color: #ffffff;
background-color: #007bff;
margin: 0 0 0 2rem;
border-radius: 8px;
text-align: center;
}
#sms-timer:hover {
background-color: #0056b3;
cursor: pointer;
}
/*.flag-dropdown button {
height: 100%;
}*/

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@ document.querySelectorAll('.email-input').forEach(input => {
document.addEventListener('DOMContentLoaded', function () {
var dropdownItems = document.querySelectorAll('.culture-dropdown-item');
dropdownItems.forEach(function (item) {
item.addEventListener('click', async function(event) {
item.addEventListener('click', async function (event) {
event.preventDefault();
var language = this.getAttribute('data-language');
var flagCode = this.getAttribute('data-flag');
@@ -21,6 +21,30 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
const setTimer = (elementId, expirationTime) => {
const element = document.getElementById(elementId);
const interval = setInterval(function () {
var now = new Date();
var diffInMillis = expirationTime - now;
if (diffInMillis <= 0) {
element.textContent = "00:00";
clearInterval(interval);
}
var minutes = Math.floor(diffInMillis / 1000 / 60);
var seconds = Math.floor((diffInMillis / 1000) % 60);
var formattedMinutes = minutes.toString().padStart(2, '0');
var formattedSeconds = seconds.toString().padStart(2, '0');
var remainingTime = `${formattedMinutes}:${formattedSeconds}`;
element.textContent = remainingTime;
}, 1000);
}
const bsNotify = (message, options) => alertify.notify(
`<div class="alert ${options.alert_type ? 'alert-' + options.alert_type : ''}" role="alert"><span class="material-symbols-outlined">${options?.icon_name ?? ''}</span><p>${message}</p></div>`,
'custom',

View File

@@ -1 +1 @@
document.querySelectorAll(".email-input").forEach(n=>{n.addEventListener("input",function(){/^\S+@\S+\.\S+$/.test(this.value)?this.classList.remove("is-invalid"):this.classList.add("is-invalid")})});document.addEventListener("DOMContentLoaded",function(){var n=document.querySelectorAll(".culture-dropdown-item");n.forEach(function(n){n.addEventListener("click",async function(n){n.preventDefault();var t=this.getAttribute("data-language"),i=this.getAttribute("data-flag");document.getElementById("selectedFlag").className="fi "+i+" me-2";await setLanguage(t)})})});const bsNotify=(n,t)=>alertify.notify(`<div class="alert ${t.alert_type?"alert-"+t.alert_type:""}" role="alert"><span class="material-symbols-outlined">${t?.icon_name??""}</span><p>${n}</p></div>`,"custom",t?.delay??5);class Comp{static ActPanel=class{static __Root;static get Root(){Comp.ActPanel.__Root??=document.getElementById("flex-action-panel");return Comp.ActPanel.__Root}static get Elements(){return[...Comp.ActPanel.Root.children]}static get IsHided(){return Comp.ActPanel.Root.style.display=="none"}static set Display(n){Comp.ActPanel.Root.style.display=n;Comp.ActPanel.Elements.forEach(t=>t.style.display=n)}static Toggle(){Comp.ActPanel.Display=Comp.ActPanel.IsHided?"":"none"}};static SignatureProgress=class{static __SignatureCount;static get SignatureCount(){this.__SignatureCount=parseInt(document.getElementById("signature-count").innerText);return this.__SignatureCount}static __SignedCountSpan;static get SignedCountSpan(){this.__SignedCountSpan??=document.getElementById("signed-count");return Comp.SignatureProgress.__SignedCountSpan}static __signedCount=0;static get SignedCount(){return this.__signedCount}static set SignedCount(n){this.__signedCount=n;const t=(n/this.SignatureCount)*100;this.SignedCountBar.style.setProperty("--progress-width",t+"%");this.SignedCountSpan.innerText=n.toString()}static __SignedCountBar;static get SignedCountBar(){this.__SignedCountBar??=document.getElementById("signed-count-bar");return this.__SignedCountBar}};static __ShareBackdrop;static get ShareBackdrop(){return Comp.__ShareBackdrop??=new bootstrap.Modal(document.getElementById("shareBackdrop")),this.__ShareBackdrop}}
document.querySelectorAll(".email-input").forEach(n=>{n.addEventListener("input",function(){/^\S+@\S+\.\S+$/.test(this.value)?this.classList.remove("is-invalid"):this.classList.add("is-invalid")})});document.addEventListener("DOMContentLoaded",function(){var n=document.querySelectorAll(".culture-dropdown-item");n.forEach(function(n){n.addEventListener("click",async function(n){n.preventDefault();var t=this.getAttribute("data-language"),i=this.getAttribute("data-flag");document.getElementById("selectedFlag").className="fi "+i+" me-2";await setLanguage(t)})})});const setTimer=(n,t)=>{const i=document.getElementById(n),r=setInterval(function(){var u=new Date,n=t-u;n<=0&&(i.textContent="00:00",clearInterval(r));var f=Math.floor(n/6e4),e=Math.floor(n/1e3%60),o=f.toString().padStart(2,"0"),s=e.toString().padStart(2,"0"),h=`${o}:${s}`;i.textContent=h},1e3)},bsNotify=(n,t)=>alertify.notify(`<div class="alert ${t.alert_type?"alert-"+t.alert_type:""}" role="alert"><span class="material-symbols-outlined">${t?.icon_name??""}</span><p>${n}</p></div>`,"custom",t?.delay??5);class Comp{static ActPanel=class{static __Root;static get Root(){Comp.ActPanel.__Root??=document.getElementById("flex-action-panel");return Comp.ActPanel.__Root}static get Elements(){return[...Comp.ActPanel.Root.children]}static get IsHided(){return Comp.ActPanel.Root.style.display=="none"}static set Display(n){Comp.ActPanel.Root.style.display=n;Comp.ActPanel.Elements.forEach(t=>t.style.display=n)}static Toggle(){Comp.ActPanel.Display=Comp.ActPanel.IsHided?"":"none"}};static SignatureProgress=class{static __SignatureCount;static get SignatureCount(){this.__SignatureCount=parseInt(document.getElementById("signature-count").innerText);return this.__SignatureCount}static __SignedCountSpan;static get SignedCountSpan(){this.__SignedCountSpan??=document.getElementById("signed-count");return Comp.SignatureProgress.__SignedCountSpan}static __signedCount=0;static get SignedCount(){return this.__signedCount}static set SignedCount(n){this.__signedCount=n;const t=(n/this.SignatureCount)*100;this.SignedCountBar.style.setProperty("--progress-width",t+"%");this.SignedCountSpan.innerText=n.toString()}static __SignedCountBar;static get SignedCountBar(){this.__SignedCountBar??=document.getElementById("signed-count-bar");return this.__SignedCountBar}};static __ShareBackdrop;static get ShareBackdrop(){return Comp.__ShareBackdrop??=new bootstrap.Modal(document.getElementById("shareBackdrop")),this.__ShareBackdrop}}