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,7 +51,9 @@ namespace EnvelopeGenerator.Application.DTOs
|
|||||||
|
|
||||||
public int? ExpiresWarningWhenDays { get; set; }
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
public bool DmzMoved { get; set; }
|
public bool TFAEnabled { get; init; }
|
||||||
|
|
||||||
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,59 +1,60 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Resources\Model.Designer.vb" />
|
<None Remove="Resources\Model.Designer.vb" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="2.2.1" />
|
<PackageReference Include="DigitalData.Core.Abstractions" Version="2.2.1" />
|
||||||
<PackageReference Include="DigitalData.Core.Application" Version="2.0.0" />
|
<PackageReference Include="DigitalData.Core.Application" Version="2.0.0" />
|
||||||
<PackageReference Include="DigitalData.Core.Client" Version="2.0.3" />
|
<PackageReference Include="DigitalData.Core.Client" Version="2.0.3" />
|
||||||
<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="QRCoder" Version="1.6.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
<PackageReference Include="UserManager.Application" Version="2.0.0" />
|
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
|
||||||
<PackageReference Include="UserManager.Infrastructure" Version="2.0.0" />
|
<PackageReference Include="UserManager.Application" Version="2.0.0" />
|
||||||
</ItemGroup>
|
<PackageReference Include="UserManager.Infrastructure" Version="2.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Extensions\EnvelopeGenerator.Extensions.csproj" />
|
<ProjectReference Include="..\EnvelopeGenerator.Extensions\EnvelopeGenerator.Extensions.csproj" />
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Resources\Model.Designer.cs">
|
<Compile Update="Resources\Model.Designer.cs">
|
||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
<DependentUpon>Model.resx</DependentUpon>
|
<DependentUpon>Model.resx</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Update="Resources\Model.en.resx">
|
<EmbeddedResource Update="Resources\Model.en.resx">
|
||||||
<CustomToolNamespace>My.Resources</CustomToolNamespace>
|
<CustomToolNamespace>My.Resources</CustomToolNamespace>
|
||||||
<LastGenOutput>Model.en.Designer.vb</LastGenOutput>
|
<LastGenOutput>Model.en.Designer.vb</LastGenOutput>
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Resources\Model.resx">
|
<EmbeddedResource Update="Resources\Model.resx">
|
||||||
<CustomToolNamespace>My.Resources</CustomToolNamespace>
|
<CustomToolNamespace>My.Resources</CustomToolNamespace>
|
||||||
<LastGenOutput>Model.Designer.cs</LastGenOutput>
|
<LastGenOutput>Model.Designer.cs</LastGenOutput>
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
|
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Resources\Resource.en-US.resx">
|
<EmbeddedResource Update="Resources\Resource.en-US.resx">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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,67 +4,46 @@ 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 SmsParams _smsParams;
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
private readonly ICodeGenerator _codeGen;
|
||||||
|
|
||||||
|
public string ServiceProvider { get; }
|
||||||
|
|
||||||
|
public GtxMessagingService(IHttpClientService<SmsParams> smsClient, IOptions<SmsParams> smsParamsOptions, IMapper mapper, ICodeGenerator codeGenerator)
|
||||||
{
|
{
|
||||||
private readonly IHttpClientService<SmsParams> _smsClient;
|
_smsClient = smsClient;
|
||||||
|
_smsParams = smsParamsOptions.Value;
|
||||||
|
_mapper = mapper;
|
||||||
|
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
|
||||||
|
_codeGen = codeGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly SmsParams _smsParams;
|
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
|
||||||
|
{
|
||||||
private readonly IMapper _mapper;
|
return await _smsClient.FetchAsync(queryParams: new Dictionary<string, object?>()
|
||||||
|
|
||||||
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.RecipientQueryParamName, recipient },
|
||||||
_smsParams = smsParamsOptions.Value;
|
{ _smsParams.MessageQueryParamName, message }
|
||||||
_mapper = mapper;
|
})
|
||||||
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
|
.ThenAsync(res => res.Json<GtxMessagingResponse>())
|
||||||
_codeGen = codeGenerator;
|
.ThenAsync(_mapper.Map<SmsResponse>);
|
||||||
_erCache = envelopeReceiverCache;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
|
public async Task<SmsResponse> SendSmsCodeAsync(string recipient, string secretKey, string? messageFormat = null)
|
||||||
{
|
{
|
||||||
return await _smsClient.FetchAsync(queryParams: new Dictionary<string, object?>()
|
var code = _codeGen.GenerateTotp(secretKey, _smsParams.SmsTotpStep);
|
||||||
{
|
var message = string.Format(messageFormat ?? _smsParams.DefaultTotpMessageFormat, code);
|
||||||
{ _smsParams.RecipientQueryParamName, recipient },
|
return await SendSmsAsync(recipient: recipient, message: message);
|
||||||
{ _smsParams.MessageQueryParamName, message }
|
|
||||||
})
|
|
||||||
.ThenAsync(res => res.Json<GtxMessagingResponse>())
|
|
||||||
.ThenAsync(_mapper.Map<SmsResponse>);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ using EnvelopeGenerator.Application.Extensions;
|
|||||||
namespace EnvelopeGenerator.Web.Controllers
|
namespace EnvelopeGenerator.Web.Controllers
|
||||||
{
|
{
|
||||||
public class HomeController : Controller
|
public class HomeController : Controller
|
||||||
{
|
{
|
||||||
private readonly ILogger<HomeController> _logger;
|
private readonly ILogger<HomeController> _logger;
|
||||||
private readonly EnvelopeOldService envelopeOldService;
|
private readonly EnvelopeOldService envelopeOldService;
|
||||||
private readonly IEnvelopeReceiverService _envRcvService;
|
private readonly IEnvelopeReceiverService _envRcvService;
|
||||||
private readonly IEnvelopeHistoryService _historyService;
|
private readonly IEnvelopeHistoryService _historyService;
|
||||||
private readonly IStringLocalizer<Resource> _localizer;
|
private readonly IStringLocalizer<Resource> _localizer;
|
||||||
@@ -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,153 +183,152 @@ 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)
|
||||||
|
{
|
||||||
|
if (viaSms)
|
||||||
|
{
|
||||||
|
//add date time cache
|
||||||
|
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, er_secret.Receiver!.TotpSecretkey!, SmsFormat);
|
||||||
|
if (res.Ok)
|
||||||
|
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", _codeGenerator.GetTotpExpirationTime(SmsTotpStep));
|
||||||
|
else if (!res.Allowed)
|
||||||
|
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", res.AllowedAt);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
async Task<IActionResult> TFAView(bool viaSms)
|
var res_json = JsonConvert.SerializeObject(res);
|
||||||
{
|
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: $"An unexpected error occurred while sending an SMS code. Response: ${res_json}");
|
||||||
if (viaSms)
|
return this.ViewInnerServiceError();
|
||||||
{
|
|
||||||
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, envelopeReceiverId: envelopeReceiverId);
|
|
||||||
if (res.Ok)
|
|
||||||
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", res.Expiration);
|
|
||||||
else if (!res.Allowed)
|
|
||||||
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return View("EnvelopeLocked").WithData("CodeType", "authenticatorCode").WithData("QRCodeExpiration", er_secret.Receiver?.TotpExpiration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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.TFAEnabled)
|
|
||||||
{
|
|
||||||
var rcv = er_secret.Receiver;
|
|
||||||
if (rcv.IsTotpSecretInvalid())
|
|
||||||
{
|
|
||||||
rcv.TotpSecretkey = _codeGenerator.GenerateTotpSecretKey();
|
|
||||||
rcv.TotpExpiration = DateTime.Now.AddMonths(1);
|
|
||||||
await _rcvService.UpdateAsync(rcv);
|
|
||||||
await _mailService.SendTFAQrCodeAsync(er_secret);
|
|
||||||
}
|
|
||||||
return await TFAView(auth.UserSelectSMS);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
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 TFAView(viaSms: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auth.HasAuthenticatorCode)
|
|
||||||
{
|
|
||||||
if (er_secret.Receiver!.IsTotpInvalid(totp: auth.AuthenticatorCode!))
|
|
||||||
{
|
|
||||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
||||||
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
|
||||||
return await TFAView(viaSms: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
||||||
return View("EnvelopeLocked")
|
|
||||||
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
//continue the process without important data to minimize security errors.
|
|
||||||
EnvelopeReceiverDto er = er_secret;
|
|
||||||
|
|
||||||
ViewData["EnvelopeKey"] = envelopeReceiverId;
|
|
||||||
//check rejection
|
|
||||||
var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id);
|
|
||||||
if(rejRcvrs.Any())
|
|
||||||
{
|
|
||||||
ViewBag.IsExt = !rejRcvrs.Contains(er.Receiver); //external if the current user is not rejected
|
|
||||||
return View("EnvelopeRejected", er);
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if it has already signed
|
|
||||||
if (await _historyService.IsSigned(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress))
|
|
||||||
return View("EnvelopeSigned");
|
|
||||||
|
|
||||||
if (er.Envelope.Documents?.FirstOrDefault() is EnvelopeDocumentDto doc && doc.ByteData is not null)
|
|
||||||
{
|
|
||||||
ViewData["DocumentBytes"] = doc.ByteData;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
|
|
||||||
return this.ViewDocumentNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var claims = new List<Claim> {
|
|
||||||
new(ClaimTypes.NameIdentifier, uuid),
|
|
||||||
new(ClaimTypes.Hash, signature),
|
|
||||||
new(ClaimTypes.Name, er.Name ?? string.Empty),
|
|
||||||
new(ClaimTypes.Email, er.Receiver.EmailAddress),
|
|
||||||
new(EnvelopeClaimTypes.Title, er.Envelope.Title),
|
|
||||||
new(EnvelopeClaimTypes.Id, er.Envelope.Id.ToString())
|
|
||||||
};
|
|
||||||
|
|
||||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
var authProperties = new AuthenticationProperties
|
|
||||||
{
|
|
||||||
AllowRefresh = false,
|
|
||||||
IsPersistent = false
|
|
||||||
};
|
|
||||||
|
|
||||||
await HttpContext.SignInAsync(
|
|
||||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
|
||||||
new ClaimsPrincipal(claimsIdentity),
|
|
||||||
authProperties);
|
|
||||||
|
|
||||||
//add PSPDFKit licence key
|
|
||||||
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
|
|
||||||
|
|
||||||
return View("ShowEnvelope", er);
|
|
||||||
},
|
|
||||||
Fail: (messages, notices) =>
|
|
||||||
{
|
|
||||||
_logger.LogNotice(notices);
|
|
||||||
return this.ViewEnvelopeNotFound();
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return View("EnvelopeLocked").WithData("CodeType", "authenticatorCode").WithData("QRCodeExpiration", er_secret.Receiver?.TotpExpiration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, EnvelopeStatus.AccessCodeIncorrect);
|
||||||
|
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
return View("EnvelopeLocked")
|
||||||
|
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeCorrect);
|
||||||
|
|
||||||
|
//check if the user has phone is added
|
||||||
|
if (er_secret.Envelope!.TFAEnabled)
|
||||||
|
{
|
||||||
|
var rcv = er_secret.Receiver;
|
||||||
|
if (rcv.IsTotpSecretInvalid())
|
||||||
|
{
|
||||||
|
rcv.TotpSecretkey = _codeGenerator.GenerateTotpSecretKey();
|
||||||
|
rcv.TotpExpiration = DateTime.Now.AddMonths(1);
|
||||||
|
await _rcvService.UpdateAsync(rcv);
|
||||||
|
await _mailService.SendTFAQrCodeAsync(er_secret);
|
||||||
|
}
|
||||||
|
return await TFAView(auth.UserSelectSMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (auth.HasSmsCode)
|
||||||
|
{
|
||||||
|
if (er_secret.Receiver!.TotpSecretkey is null)
|
||||||
|
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
|
||||||
|
|
||||||
|
if (_codeGenerator.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey, step: SmsTotpStep))
|
||||||
|
{
|
||||||
|
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
||||||
|
return await TFAView(viaSms: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auth.HasAuthenticatorCode)
|
||||||
|
{
|
||||||
|
if (er_secret.Receiver!.IsTotpInvalid(totp: auth.AuthenticatorCode!))
|
||||||
|
{
|
||||||
|
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
||||||
|
return await TFAView(viaSms: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
return View("EnvelopeLocked")
|
||||||
|
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//continue the process without important data to minimize security errors.
|
||||||
|
EnvelopeReceiverDto er = er_secret;
|
||||||
|
|
||||||
|
ViewData["EnvelopeKey"] = envelopeReceiverId;
|
||||||
|
//check rejection
|
||||||
|
var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id);
|
||||||
|
if(rejRcvrs.Any())
|
||||||
|
{
|
||||||
|
ViewBag.IsExt = !rejRcvrs.Contains(er.Receiver); //external if the current user is not rejected
|
||||||
|
return View("EnvelopeRejected", er);
|
||||||
|
}
|
||||||
|
|
||||||
|
//check if it has already signed
|
||||||
|
if (await _historyService.IsSigned(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress))
|
||||||
|
return View("EnvelopeSigned");
|
||||||
|
|
||||||
|
if (er.Envelope.Documents?.FirstOrDefault() is EnvelopeDocumentDto doc && doc.ByteData is not null)
|
||||||
|
{
|
||||||
|
ViewData["DocumentBytes"] = doc.ByteData;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
|
||||||
|
return this.ViewDocumentNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = new List<Claim> {
|
||||||
|
new(ClaimTypes.NameIdentifier, uuid),
|
||||||
|
new(ClaimTypes.Hash, signature),
|
||||||
|
new(ClaimTypes.Name, er.Name ?? string.Empty),
|
||||||
|
new(ClaimTypes.Email, er.Receiver.EmailAddress),
|
||||||
|
new(EnvelopeClaimTypes.Title, er.Envelope.Title),
|
||||||
|
new(EnvelopeClaimTypes.Id, er.Envelope.Id.ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
var authProperties = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
AllowRefresh = false,
|
||||||
|
IsPersistent = false
|
||||||
|
};
|
||||||
|
|
||||||
|
await HttpContext.SignInAsync(
|
||||||
|
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
new ClaimsPrincipal(claimsIdentity),
|
||||||
|
authProperties);
|
||||||
|
|
||||||
|
//add PSPDFKit licence key
|
||||||
|
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
|
||||||
|
|
||||||
|
return View("ShowEnvelope", er);
|
||||||
}
|
}
|
||||||
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