Compare commits
9 Commits
5a0e258b35
...
cf300d3ade
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf300d3ade | ||
|
|
be44f9f436 | ||
|
|
80f9107e4e | ||
|
|
c6e9ecfbca | ||
|
|
af5d7c289d | ||
|
|
3267acbeb3 | ||
|
|
95efe58e1b | ||
|
|
867756242e | ||
|
|
713c2f3ed2 |
@@ -13,7 +13,5 @@
|
|||||||
/// The placeholder {0} represents the envelopeReceiverId.
|
/// The placeholder {0} represents the envelopeReceiverId.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CodeExpirationCacheKeyFormat { get; init; } = "sms-code-expiration-{0}";
|
public string CodeExpirationCacheKeyFormat { get; init; } = "sms-code-expiration-{0}";
|
||||||
|
|
||||||
public TimeSpan CodeCacheValidityPeriod { get; init; } = new(0, 5, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using DigitalData.Core.Abstractions.Client;
|
using DigitalData.Core.Abstractions.Client;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using OtpNet;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
|
namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
|
||||||
{
|
{
|
||||||
@@ -21,5 +22,9 @@ namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
|
|||||||
public string MessageQueryParamName { get; init; } = "text";
|
public string MessageQueryParamName { get; init; } = "text";
|
||||||
|
|
||||||
public int CodeLength { get; init; } = 5;
|
public int CodeLength { get; init; } = 5;
|
||||||
|
|
||||||
|
public int SmsTotpStep { get; init; } = 300;
|
||||||
|
|
||||||
|
public string DefaultTotpMessageFormat { get; init; } = "{0}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
namespace EnvelopeGenerator.Application.Contracts
|
using OtpNet;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Contracts
|
||||||
{
|
{
|
||||||
public interface ICodeGenerator
|
public interface ICodeGenerator
|
||||||
{
|
{
|
||||||
string GenerateCode(int length);
|
string GenerateCode(int length);
|
||||||
|
|
||||||
public string GenerateTotpSecretKey(int? length = null);
|
string GenerateTotpSecretKey(int? length = null);
|
||||||
|
|
||||||
public byte[] GenerateTotpQrCode(string userEmail, string secretKey, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null);
|
byte[] GenerateTotpQrCode(string userEmail, string secretKey, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null);
|
||||||
|
|
||||||
public byte[] GenerateTotpQrCode(string userEmail, int? length = null, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null);
|
byte[] GenerateTotpQrCode(string userEmail, int? length = null, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null);
|
||||||
|
|
||||||
|
string GenerateTotp(string secretKey, int step = 30);
|
||||||
|
|
||||||
|
bool VerifyTotp(string totpCode, string secretKey, int step = 30, VerificationWindow? window = null);
|
||||||
|
|
||||||
|
bool GetTotpExpirationTime(int step = 30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Contracts
|
namespace EnvelopeGenerator.Application.Contracts;
|
||||||
|
|
||||||
|
public interface IMessagingService
|
||||||
{
|
{
|
||||||
public interface IMessagingService
|
|
||||||
{
|
|
||||||
string ServiceProvider { get; }
|
string ServiceProvider { get; }
|
||||||
|
|
||||||
Task<SmsResponse> SendSmsAsync(string recipient, string message);
|
Task<SmsResponse> SendSmsAsync(string recipient, string message);
|
||||||
|
|
||||||
Task<SmsResponse> SendSmsCodeAsync(string recipient, string envelopeReceiverId);
|
Task<SmsResponse> SendSmsCodeAsync(string recipient, string secretKey, string messageFormat = "{0}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,8 @@ namespace EnvelopeGenerator.Application.DTOs
|
|||||||
|
|
||||||
public int? ExpiresWarningWhenDays { get; set; }
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
|
public bool TFAEnabled { get; init; }
|
||||||
|
|
||||||
public bool DmzMoved { get; set; }
|
public bool DmzMoved { get; set; }
|
||||||
public UserReadDto? User { get; set; }
|
public UserReadDto? User { get; set; }
|
||||||
public EnvelopeType? EnvelopeType { get; set; }
|
public EnvelopeType? EnvelopeType { get; set; }
|
||||||
|
|||||||
@@ -27,7 +27,5 @@ namespace EnvelopeGenerator.Application.DTOs.EnvelopeReceiver
|
|||||||
public DateTime? ChangedWhen { get; init; }
|
public DateTime? ChangedWhen { get; init; }
|
||||||
|
|
||||||
public bool HasPhoneNumber { get; init; }
|
public bool HasPhoneNumber { get; init; }
|
||||||
|
|
||||||
public bool TFAEnabled { get; init; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Domain.HttpResponse
|
namespace EnvelopeGenerator.Application.DTOs.Messaging
|
||||||
{
|
{
|
||||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||||
}
|
}
|
||||||
@@ -4,16 +4,6 @@
|
|||||||
{
|
{
|
||||||
public required bool Ok { get; init; }
|
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;
|
|
||||||
|
|
||||||
public bool Allowed => AllowedAt is null || DateTime.Now >= AllowedAt;
|
|
||||||
|
|
||||||
public bool Error => !Ok && Allowed;
|
|
||||||
|
|
||||||
public dynamic? Errors { get; init; }
|
public dynamic? Errors { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<PackageReference Include="DigitalData.Core.DTO" Version="2.0.0" />
|
<PackageReference Include="DigitalData.Core.DTO" Version="2.0.0" />
|
||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="2.0.0" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
||||||
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
|
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
|
||||||
<PackageReference Include="UserManager.Application" Version="2.0.0" />
|
<PackageReference Include="UserManager.Application" Version="2.0.0" />
|
||||||
|
|||||||
@@ -4,79 +4,128 @@ namespace EnvelopeGenerator.Application.Extensions
|
|||||||
{
|
{
|
||||||
public static class CacheExtensions
|
public static class CacheExtensions
|
||||||
{
|
{
|
||||||
public static Task SetLongAsync(this IDistributedCache cache, string key, long value, DistributedCacheEntryOptions? options = null)
|
public static Task SetLongAsync(this IDistributedCache cache, string key, long value, DistributedCacheEntryOptions? options = null, CancellationToken cToken = default)
|
||||||
=> options is null
|
=> options is null
|
||||||
? cache.SetAsync(key, BitConverter.GetBytes(value))
|
? cache.SetAsync(key, BitConverter.GetBytes(value), token: cToken)
|
||||||
: cache.SetAsync(key, BitConverter.GetBytes(value), options: options);
|
: cache.SetAsync(key, BitConverter.GetBytes(value), options: options, token: cToken);
|
||||||
|
|
||||||
public static async Task<long?> GetLongAsync(this IDistributedCache cache, string key)
|
public static async Task<long?> GetLongAsync(this IDistributedCache cache, string key, CancellationToken cToken = default)
|
||||||
{
|
{
|
||||||
var value = await cache.GetAsync(key);
|
var value = await cache.GetAsync(key, cToken);
|
||||||
return value is null ? null : BitConverter.ToInt64(value, 0);
|
return value is null ? null : BitConverter.ToInt64(value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task SetDateTimeAsync(this IDistributedCache cache, string key, DateTime value, DistributedCacheEntryOptions? options = null)
|
public static Task SetDateTimeAsync(this IDistributedCache cache, string key, DateTime value, DistributedCacheEntryOptions? options = null, CancellationToken cToken = default)
|
||||||
=> cache.SetLongAsync(key: key, value: value.Ticks, options: options);
|
=> cache.SetLongAsync(key: key, value: value.Ticks, options: options, cToken: cToken);
|
||||||
|
|
||||||
public static async Task<DateTime?> GetDateTimeAsync(this IDistributedCache cache, string key)
|
public static async Task<DateTime?> GetDateTimeAsync(this IDistributedCache cache, string key, CancellationToken cToken = default)
|
||||||
{
|
{
|
||||||
var value = await cache.GetAsync(key);
|
var value = await cache.GetAsync(key, cToken);
|
||||||
return value is null ? null : new(BitConverter.ToInt64(value, 0));
|
return value is null ? null : new(BitConverter.ToInt64(value, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task SetTimeSpanAsync(this IDistributedCache cache, string key, TimeSpan value, DistributedCacheEntryOptions? options = null)
|
public static Task SetTimeSpanAsync(this IDistributedCache cache, string key, TimeSpan value, DistributedCacheEntryOptions? options = null, CancellationToken cToken = default)
|
||||||
=> cache.SetLongAsync(key: key, value: value.Ticks, options: options);
|
=> cache.SetLongAsync(key: key, value: value.Ticks, options: options, cToken);
|
||||||
|
|
||||||
public static async Task<TimeSpan?> GetTimeSpanAsync(this IDistributedCache cache, string key)
|
public static async Task<TimeSpan?> GetTimeSpanAsync(this IDistributedCache cache, string key, CancellationToken cToken = default)
|
||||||
{
|
{
|
||||||
var value = await cache.GetAsync(key);
|
var value = await cache.GetAsync(key, cToken);
|
||||||
return value is null ? null : new(BitConverter.ToInt64(value, 0));
|
return value is null ? null : new(BitConverter.ToInt64(value, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetOrSet(this IDistributedCache cache, string key, Func<string> factory, DistributedCacheEntryOptions? options = null, bool cacheInBackground = false, CancellationToken token = default)
|
//TODO: use code generator
|
||||||
|
#region GetOrSetAsync
|
||||||
|
|
||||||
|
#region string
|
||||||
|
public static async Task<string> GetOrSetAsync(this IDistributedCache cache, string key, Func<string> factory, DistributedCacheEntryOptions? options = null, bool cacheInBackground = false, CancellationToken cToken = default)
|
||||||
{
|
{
|
||||||
var value = cache.GetString(key);
|
var value = await cache.GetStringAsync(key, cToken);
|
||||||
if (value is null)
|
if (value is null)
|
||||||
{
|
{
|
||||||
// create new and save
|
// create new and save
|
||||||
value = factory();
|
value = factory();
|
||||||
|
|
||||||
void Cache()
|
|
||||||
{
|
|
||||||
if (options is null)
|
|
||||||
cache.SetString(key: key, value: value);
|
|
||||||
else
|
|
||||||
cache.SetString(key: key, value: value, options: options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheInBackground)
|
|
||||||
_ = Task.Run(() => Cache(), token);
|
|
||||||
else
|
|
||||||
Cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> GetOrSetAsync(this IDistributedCache cache, string key, Func<Task<string>> factory, DistributedCacheEntryOptions? options = null, bool cacheInBackground = false, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
var value = await cache.GetStringAsync(key, token: token);
|
|
||||||
if(value is null)
|
|
||||||
{
|
|
||||||
// create new and save
|
|
||||||
value = await factory();
|
|
||||||
|
|
||||||
Task CacheAsync() => options is null
|
Task CacheAsync() => options is null
|
||||||
? cache.SetStringAsync(key: key, value: value, token: token)
|
? cache.SetStringAsync(key, value, cToken)
|
||||||
: cache.SetStringAsync(key: key, value: value, options: options, token: token);
|
: cache.SetStringAsync(key, value, options, cToken);
|
||||||
|
|
||||||
if (cacheInBackground)
|
if (cacheInBackground)
|
||||||
_ = Task.Run(async () => await CacheAsync(), token);
|
_ = Task.Run(async () => await CacheAsync(), cToken);
|
||||||
else
|
else
|
||||||
await CacheAsync();
|
await CacheAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetOrSetAsync(this IDistributedCache cache, string key, Func<Task<string>> factoryAsync, DistributedCacheEntryOptions? options = null, bool cacheInBackground = false, CancellationToken cToken = default)
|
||||||
|
{
|
||||||
|
var value = await cache.GetStringAsync(key, cToken);
|
||||||
|
if(value is null)
|
||||||
|
{
|
||||||
|
// create new and save
|
||||||
|
value = await factoryAsync();
|
||||||
|
|
||||||
|
Task CacheAsync() => options is null
|
||||||
|
? cache.SetStringAsync(key: key, value: value, token: cToken)
|
||||||
|
: cache.SetStringAsync(key: key, value: value, options: options, token: cToken);
|
||||||
|
|
||||||
|
if (cacheInBackground)
|
||||||
|
_ = Task.Run(async () => await CacheAsync(), cToken);
|
||||||
|
else
|
||||||
|
await CacheAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DateTime
|
||||||
|
public static async Task<DateTime> GetOrSetAsync(this IDistributedCache cache, string key, Func<DateTime> factory, DistributedCacheEntryOptions? options = null, bool cacheInBackground = false, CancellationToken cToken = default)
|
||||||
|
{
|
||||||
|
if (await cache.GetDateTimeAsync(key, cToken) is DateTime dateTimeValue)
|
||||||
|
return dateTimeValue;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// create new and save
|
||||||
|
var newValue = factory();
|
||||||
|
|
||||||
|
Task CacheAsync() => options is null
|
||||||
|
? cache.SetDateTimeAsync(key, newValue, cToken: cToken)
|
||||||
|
: cache.SetDateTimeAsync(key, newValue, options, cToken);
|
||||||
|
|
||||||
|
if (cacheInBackground)
|
||||||
|
_ = Task.Run(async () => await CacheAsync(), cToken);
|
||||||
|
else
|
||||||
|
await CacheAsync();
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DateTime> GetOrSetAsync(this IDistributedCache cache, string key, Func<Task<DateTime>> factory, DistributedCacheEntryOptions? options = null, bool cacheInBackground = false, CancellationToken cToken = default)
|
||||||
|
{
|
||||||
|
if (await cache.GetDateTimeAsync(key, cToken) is DateTime dateTimeValue)
|
||||||
|
return dateTimeValue;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// create new and save
|
||||||
|
var newValue = await factory();
|
||||||
|
|
||||||
|
Task CacheAsync() => options is null
|
||||||
|
? cache.SetDateTimeAsync(key, newValue, cToken: cToken)
|
||||||
|
: cache.SetDateTimeAsync(key, newValue, options, cToken);
|
||||||
|
|
||||||
|
if (cacheInBackground)
|
||||||
|
_ = Task.Run(async () => await CacheAsync(), cToken);
|
||||||
|
else
|
||||||
|
await CacheAsync();
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using EnvelopeGenerator.Domain.HttpResponse;
|
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Extensions
|
namespace EnvelopeGenerator.Application.Extensions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using EnvelopeGenerator.Application.DTOs.Messaging;
|
|||||||
using EnvelopeGenerator.Application.DTOs.Receiver;
|
using EnvelopeGenerator.Application.DTOs.Receiver;
|
||||||
using EnvelopeGenerator.Application.Extensions;
|
using EnvelopeGenerator.Application.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.HttpResponse;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.MappingProfiles
|
namespace EnvelopeGenerator.Application.MappingProfiles
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -62,5 +62,15 @@ namespace EnvelopeGenerator.Application.Services
|
|||||||
totpUrlFormat: totpUrlFormat,
|
totpUrlFormat: totpUrlFormat,
|
||||||
pixelsPerModule: pixelsPerModule);
|
pixelsPerModule: pixelsPerModule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GenerateTotp(string secretKey, int step = 30) => new Totp(Base32Encoding.ToBytes(secretKey), step).ComputeTotp();
|
||||||
|
|
||||||
|
public bool VerifyTotp(string totpCode, string secretKey, int step = 30, VerificationWindow? window = null)
|
||||||
|
=> new Totp(Base32Encoding.ToBytes(secretKey), step).VerifyTotp(totpCode, out _, window);
|
||||||
|
|
||||||
|
public bool GetTotpExpirationTime(int step = 30)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,12 @@ using DigitalData.Core.Client;
|
|||||||
using EnvelopeGenerator.Application.Configurations.GtxMessaging;
|
using EnvelopeGenerator.Application.Configurations.GtxMessaging;
|
||||||
using EnvelopeGenerator.Application.Contracts;
|
using EnvelopeGenerator.Application.Contracts;
|
||||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||||
using EnvelopeGenerator.Application.Extensions;
|
|
||||||
using EnvelopeGenerator.Domain.HttpResponse;
|
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Services
|
namespace EnvelopeGenerator.Application.Services;
|
||||||
|
|
||||||
|
public class GtxMessagingService : IMessagingService
|
||||||
{
|
{
|
||||||
public class GtxMessagingService : IMessagingService
|
|
||||||
{
|
|
||||||
private readonly IHttpClientService<SmsParams> _smsClient;
|
private readonly IHttpClientService<SmsParams> _smsClient;
|
||||||
|
|
||||||
private readonly SmsParams _smsParams;
|
private readonly SmsParams _smsParams;
|
||||||
@@ -21,18 +18,15 @@ namespace EnvelopeGenerator.Application.Services
|
|||||||
|
|
||||||
private readonly ICodeGenerator _codeGen;
|
private readonly ICodeGenerator _codeGen;
|
||||||
|
|
||||||
private readonly IEnvelopeReceiverCache _erCache;
|
|
||||||
|
|
||||||
public string ServiceProvider { get; }
|
public string ServiceProvider { get; }
|
||||||
|
|
||||||
public GtxMessagingService(IHttpClientService<SmsParams> smsClient, IOptions<SmsParams> smsParamsOptions, IMapper mapper, ICodeGenerator codeGenerator, IEnvelopeReceiverCache envelopeReceiverCache)
|
public GtxMessagingService(IHttpClientService<SmsParams> smsClient, IOptions<SmsParams> smsParamsOptions, IMapper mapper, ICodeGenerator codeGenerator)
|
||||||
{
|
{
|
||||||
_smsClient = smsClient;
|
_smsClient = smsClient;
|
||||||
_smsParams = smsParamsOptions.Value;
|
_smsParams = smsParamsOptions.Value;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
|
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
|
||||||
_codeGen = codeGenerator;
|
_codeGen = codeGenerator;
|
||||||
_erCache = envelopeReceiverCache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
|
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
|
||||||
@@ -46,25 +40,10 @@ namespace EnvelopeGenerator.Application.Services
|
|||||||
.ThenAsync(_mapper.Map<SmsResponse>);
|
.ThenAsync(_mapper.Map<SmsResponse>);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SmsResponse> SendSmsCodeAsync(string recipient, string envelopeReceiverId)
|
public async Task<SmsResponse> SendSmsCodeAsync(string recipient, string secretKey, string? messageFormat = null)
|
||||||
{
|
{
|
||||||
var code = await _erCache.GetSmsCodeAsync(envelopeReceiverId);
|
var code = _codeGen.GenerateTotp(secretKey, _smsParams.SmsTotpStep);
|
||||||
|
var message = string.Format(messageFormat ?? _smsParams.DefaultTotpMessageFormat, code);
|
||||||
if (code is null)
|
return await SendSmsAsync(recipient: recipient, message: message);
|
||||||
{
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +84,9 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
[Column("EXPIRES_WARNING_WHEN_DAYS")]
|
[Column("EXPIRES_WARNING_WHEN_DAYS")]
|
||||||
public int? ExpiresWarningWhenDays { get; set; }
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
|
[Column("TFA_ENABLED", TypeName = "bit")]
|
||||||
|
public bool TFAEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sender of envelope
|
/// The sender of envelope
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
[RegularExpression(@"^\+[0-9]+$", ErrorMessage = "Phone number must start with '+' followed by digits.")]
|
[RegularExpression(@"^\+[0-9]+$", ErrorMessage = "Phone number must start with '+' followed by digits.")]
|
||||||
public string? PhoneNumber { get; set; }
|
public string? PhoneNumber { get; set; }
|
||||||
|
|
||||||
[Column("TFA_ENABLED", TypeName = "bit")]
|
|
||||||
public bool TFAEnabled { get; set; }
|
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public (int Envelope, int Receiver) Id => (Envelope: EnvelopeId, Receiver: ReceiverId);
|
public (int Envelope, int Receiver) Id => (Envelope: EnvelopeId, Receiver: ReceiverId);
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,12 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
private readonly IEnvelopeMailService _mailService;
|
private readonly IEnvelopeMailService _mailService;
|
||||||
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
|
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
|
||||||
private readonly IMessagingService _msgService;
|
private readonly IMessagingService _msgService;
|
||||||
private readonly IEnvelopeReceiverCache _erCache;
|
|
||||||
private readonly ICodeGenerator _codeGenerator;
|
private readonly ICodeGenerator _codeGenerator;
|
||||||
private readonly IReceiverService _rcvService;
|
private readonly IReceiverService _rcvService;
|
||||||
|
private static readonly int SmsTotpStep = 60 * 3;
|
||||||
|
private static readonly string SmsFormat = "{0}";
|
||||||
|
|
||||||
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, ICodeGenerator codeGenerator, IReceiverService receiverService)
|
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, ICodeGenerator codeGenerator, IReceiverService receiverService)
|
||||||
{
|
{
|
||||||
this.envelopeOldService = envelopeOldService;
|
this.envelopeOldService = envelopeOldService;
|
||||||
_envRcvService = envelopeReceiverService;
|
_envRcvService = envelopeReceiverService;
|
||||||
@@ -53,7 +54,6 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_readOnlyService = readOnlyService;
|
_readOnlyService = readOnlyService;
|
||||||
_msgService = messagingService;
|
_msgService = messagingService;
|
||||||
_erCache = envelopeReceiverCache;
|
|
||||||
_codeGenerator = codeGenerator;
|
_codeGenerator = codeGenerator;
|
||||||
_rcvService = receiverService;
|
_rcvService = receiverService;
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync(
|
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync(
|
||||||
Success: er => View()
|
Success: er => View()
|
||||||
.WithData("EnvelopeKey", envelopeReceiverId)
|
.WithData("EnvelopeKey", envelopeReceiverId)
|
||||||
.WithData("TFAEnabled", er.TFAEnabled)
|
.WithData("TFAEnabled", er.Envelope!.TFAEnabled)
|
||||||
.WithData("HasPhoneNumber", er.HasPhoneNumber),
|
.WithData("HasPhoneNumber", er.HasPhoneNumber),
|
||||||
Fail: IActionResult (messages, notices) =>
|
Fail: IActionResult (messages, notices) =>
|
||||||
{
|
{
|
||||||
@@ -183,16 +183,23 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
//check access code
|
//check access code
|
||||||
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId);
|
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId);
|
||||||
|
|
||||||
return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync(
|
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||||
SuccessAsync: async er_secret =>
|
|
||||||
|
if (er_secret_res.IsFailed)
|
||||||
{
|
{
|
||||||
|
_logger.LogNotice(er_secret_res.Notices);
|
||||||
|
return this.ViewEnvelopeNotFound();
|
||||||
|
}
|
||||||
|
var er_secret = er_secret_res.Data;
|
||||||
|
|
||||||
async Task<IActionResult> TFAView(bool viaSms)
|
async Task<IActionResult> TFAView(bool viaSms)
|
||||||
{
|
{
|
||||||
if (viaSms)
|
if (viaSms)
|
||||||
{
|
{
|
||||||
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, envelopeReceiverId: envelopeReceiverId);
|
//add date time cache
|
||||||
|
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, er_secret.Receiver!.TotpSecretkey!, SmsFormat);
|
||||||
if (res.Ok)
|
if (res.Ok)
|
||||||
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", res.Expiration);
|
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", _codeGenerator.GetTotpExpirationTime(SmsTotpStep));
|
||||||
else if (!res.Allowed)
|
else if (!res.Allowed)
|
||||||
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", res.AllowedAt);
|
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", res.AllowedAt);
|
||||||
else
|
else
|
||||||
@@ -229,7 +236,7 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeCorrect);
|
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeCorrect);
|
||||||
|
|
||||||
//check if the user has phone is added
|
//check if the user has phone is added
|
||||||
if (er_secret.TFAEnabled)
|
if (er_secret.Envelope!.TFAEnabled)
|
||||||
{
|
{
|
||||||
var rcv = er_secret.Receiver;
|
var rcv = er_secret.Receiver;
|
||||||
if (rcv.IsTotpSecretInvalid())
|
if (rcv.IsTotpSecretInvalid())
|
||||||
@@ -245,11 +252,10 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
}
|
}
|
||||||
else if (auth.HasSmsCode)
|
else if (auth.HasSmsCode)
|
||||||
{
|
{
|
||||||
var smsCode = await _erCache.GetSmsCodeAsync(envelopeReceiverId);
|
if (er_secret.Receiver!.TotpSecretkey is null)
|
||||||
if (smsCode is null)
|
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
|
||||||
return RedirectToAction("EnvelopeLocked", new { envelopeReceiverId });
|
|
||||||
|
|
||||||
if(auth.SmsCode != smsCode)
|
if (_codeGenerator.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey, step: SmsTotpStep))
|
||||||
{
|
{
|
||||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
||||||
@@ -323,13 +329,6 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
|
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
|
||||||
|
|
||||||
return View("ShowEnvelope", er);
|
return View("ShowEnvelope", er);
|
||||||
},
|
|
||||||
Fail: (messages, notices) =>
|
|
||||||
{
|
|
||||||
_logger.LogNotice(notices);
|
|
||||||
return this.ViewEnvelopeNotFound();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -384,7 +383,8 @@ namespace EnvelopeGenerator.Web.Controllers
|
|||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId).ThenAsync(
|
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId).ThenAsync(
|
||||||
SuccessAsync: async (er) =>
|
SuccessAsync: async (er) =>
|
||||||
{ViewData["UserCulture"] = _cultures[UserLanguage];
|
{
|
||||||
|
ViewData["UserCulture"] = _cultures[UserLanguage];
|
||||||
ViewData["UserCulture"] = _cultures[UserLanguage];
|
ViewData["UserCulture"] = _cultures[UserLanguage];
|
||||||
return await _historyService.IsRejected(envelopeId: er.EnvelopeId)
|
return await _historyService.IsRejected(envelopeId: er.EnvelopeId)
|
||||||
? View(er)
|
? View(er)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<PackageId>EnvelopeGenerator.Web</PackageId>
|
<PackageId>EnvelopeGenerator.Web</PackageId>
|
||||||
<Version>2.8.1</Version>
|
<Version>2.8.2</Version>
|
||||||
<Authors>Digital Data GmbH</Authors>
|
<Authors>Digital Data GmbH</Authors>
|
||||||
<Company>Digital Data GmbH</Company>
|
<Company>Digital Data GmbH</Company>
|
||||||
<Product>EnvelopeGenerator.Web</Product>
|
<Product>EnvelopeGenerator.Web</Product>
|
||||||
|
|||||||
Reference in New Issue
Block a user