Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator
This commit is contained in:
commit
c63f369bd6
@ -1,6 +1,6 @@
|
||||
namespace EnvelopeGenerator.Application.Configurations
|
||||
{
|
||||
public class CodeGeneratorParams
|
||||
public class AuthenticatorParams
|
||||
{
|
||||
public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789";
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Configurations
|
||||
{
|
||||
public class DispatcherConfig
|
||||
{
|
||||
public int SendingProfile { get; init; } = 1;
|
||||
|
||||
public string AddedWho { get; init; } = "DDEnvelopGenerator";
|
||||
|
||||
public int ReminderTypeId { get; init; } = 202377;
|
||||
|
||||
public string EmailAttmt1 { get; init; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
namespace EnvelopeGenerator.Application.Configurations;
|
||||
|
||||
public class DispatcherParams
|
||||
{
|
||||
public int SendingProfile { get; init; } = 1;
|
||||
|
||||
public string AddedWho { get; init; } = "DDEnvelopGenerator";
|
||||
|
||||
public int ReminderTypeId { get; init; } = 202377;
|
||||
|
||||
public string EmailAttmt1 { get; init; } = string.Empty;
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Client;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
|
||||
{
|
||||
/// <summary>
|
||||
/// https://www.gtx-messaging.com/en/api-docs/sms-rest-api/
|
||||
/// </summary>
|
||||
public class SmsParams : IHttpClientOptions
|
||||
{
|
||||
public required string Uri { get; init; }
|
||||
|
||||
public string? Path { get; init; }
|
||||
|
||||
public Dictionary<string, object>? Headers { get; init; }
|
||||
|
||||
public Dictionary<string, object?>? QueryParams { get; init; }
|
||||
|
||||
public string RecipientQueryParamName { get; init; } = "to";
|
||||
|
||||
public string MessageQueryParamName { get; init; } = "text";
|
||||
|
||||
public int CodeLength { get; init; } = 5;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using DigitalData.Core.Abstractions.Client;
|
||||
namespace EnvelopeGenerator.Application.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// https://www.gtx-messaging.com/en/api-docs/sms-rest-api/
|
||||
/// </summary>
|
||||
public class GtxMessagingParams : IHttpClientOptions
|
||||
{
|
||||
public required string Uri { get; init; }
|
||||
|
||||
public string? Path { get; init; }
|
||||
|
||||
public Dictionary<string, object>? Headers { get; init; }
|
||||
|
||||
public Dictionary<string, object?>? QueryParams { get; init; }
|
||||
|
||||
public string RecipientQueryParamName { get; init; } = "to";
|
||||
|
||||
public string MessageQueryParamName { get; init; } = "text";
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Configurations
|
||||
{
|
||||
public class MailConfig
|
||||
{
|
||||
public required Dictionary<string, string> Placeholders { get; init; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.Application.Configurations;
|
||||
|
||||
public class MailParams
|
||||
{
|
||||
public required Dictionary<string, string> Placeholders { get; init; }
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
using OtpNet;
|
||||
using System.Globalization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Configurations
|
||||
{
|
||||
public class TotpSmsParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The unit is second.
|
||||
/// </summary>
|
||||
public int TotpStep { get; init; } = 90;
|
||||
|
||||
public string Format { get; init; } = "Ihr 2FA-Passwort lautet {0}. Gültig bis {1}";
|
||||
|
||||
public ExpirationHandler Expiration { get; init; } = new();
|
||||
|
||||
public VerificationWindow? TotpVerificationWindow { get; private init; } = VerificationWindow.RfcSpecifiedNetworkDelay;
|
||||
|
||||
private IEnumerable<int>? _tvwParams;
|
||||
|
||||
public IEnumerable<int>? TotpVerificationWindowParams
|
||||
{
|
||||
get => _tvwParams;
|
||||
init
|
||||
{
|
||||
_tvwParams = value;
|
||||
if(_tvwParams is not null)
|
||||
TotpVerificationWindow = new(previous: _tvwParams.ElementAtOrDefault(0), future: _tvwParams.ElementAtOrDefault(0));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExpirationHandler
|
||||
{
|
||||
public string CacheKeyFormat { get; init; } = "e{0}_r{1}_sms_code_expiration";
|
||||
|
||||
public string Format { get; init; } = "HH:mm:ss";
|
||||
|
||||
public string CultureName
|
||||
{
|
||||
get => _cultureInfo.Name;
|
||||
init => _cultureInfo = new(value);
|
||||
}
|
||||
|
||||
private CultureInfo _cultureInfo = new("de-DE");
|
||||
|
||||
public CultureInfo CultureInfo => _cultureInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
EnvelopeGenerator.Application/Contracts/IAuthenticator.cs
Normal file
19
EnvelopeGenerator.Application/Contracts/IAuthenticator.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using OtpNet;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Contracts
|
||||
{
|
||||
public interface IAuthenticator
|
||||
{
|
||||
string GenerateCode(int length);
|
||||
|
||||
string GenerateTotpSecretKey(int? length = null);
|
||||
|
||||
byte[] GenerateTotpQrCode(string userEmail, string secretKey, 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);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Contracts
|
||||
{
|
||||
public interface ICodeGenerator
|
||||
{
|
||||
string GenerateCode(int length);
|
||||
|
||||
public string GenerateTotpSecretKey(int? length = null);
|
||||
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Contracts;
|
||||
|
||||
public interface IEnvelopeSmsHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// If expiration is passed then, sends sms and returns smsResponse and up-to-date expiration; otherwise send expiration.
|
||||
/// </summary>
|
||||
/// <param name="er_secret"></param>
|
||||
/// <param name="cToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(SmsResponse? SmsResponse, DateTime Expiration)> SendTotpAsync(EnvelopeReceiverSecretDto er_secret, CancellationToken cToken = default);
|
||||
|
||||
bool VerifyTotp(string totpCode, string secretKey);
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Contracts
|
||||
{
|
||||
public interface IJWTService<TClaimValue>
|
||||
{
|
||||
public static SymmetricSecurityKey GenerateSecurityKey(int byteSize = 32)
|
||||
{
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
var randomBytes = new byte[byteSize];
|
||||
rng.GetBytes(randomBytes);
|
||||
var securityKey = new SymmetricSecurityKey(randomBytes);
|
||||
|
||||
return securityKey;
|
||||
}
|
||||
|
||||
string GenerateToken(TClaimValue claimValue);
|
||||
|
||||
JwtSecurityToken? ReadSecurityToken(string token);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Contracts
|
||||
{
|
||||
public interface IMessagingService
|
||||
{
|
||||
string ServiceProvider { get; }
|
||||
|
||||
Task<SmsResponse> SendSmsAsync(string recipient, string message);
|
||||
|
||||
Task<SmsResponse> SendSmsCodeAsync(string recipient, string envelopeReceiverId);
|
||||
}
|
||||
}
|
||||
11
EnvelopeGenerator.Application/Contracts/ISmsSender.cs
Normal file
11
EnvelopeGenerator.Application/Contracts/ISmsSender.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Contracts;
|
||||
|
||||
//TODO: move to DigitalData.Core
|
||||
public interface ISmsSender
|
||||
{
|
||||
string ServiceProvider { get; }
|
||||
|
||||
Task<SmsResponse> SendSmsAsync(string recipient, string message);
|
||||
}
|
||||
@ -51,6 +51,8 @@ namespace EnvelopeGenerator.Application.DTOs
|
||||
|
||||
public int? ExpiresWarningWhenDays { get; set; }
|
||||
|
||||
public bool TFAEnabled { get; init; }
|
||||
|
||||
public bool DmzMoved { get; set; }
|
||||
public UserReadDto? User { get; set; }
|
||||
public EnvelopeType? EnvelopeType { get; set; }
|
||||
|
||||
@ -27,7 +27,5 @@ namespace EnvelopeGenerator.Application.DTOs.EnvelopeReceiver
|
||||
public DateTime? ChangedWhen { 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?> { }
|
||||
}
|
||||
@ -4,15 +4,7 @@
|
||||
{
|
||||
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 bool Failed => !Ok;
|
||||
|
||||
public dynamic? Errors { get; init; }
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
<PackageReference Include="DigitalData.Core.DTO" Version="2.0.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="2.0.0" />
|
||||
<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-ImageSharp" Version="0.10.0" />
|
||||
<PackageReference Include="UserManager.Application" Version="2.0.0" />
|
||||
|
||||
@ -4,79 +4,128 @@ namespace EnvelopeGenerator.Application.Extensions
|
||||
{
|
||||
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
|
||||
? cache.SetAsync(key, BitConverter.GetBytes(value))
|
||||
: cache.SetAsync(key, BitConverter.GetBytes(value), options: options);
|
||||
? cache.SetAsync(key, BitConverter.GetBytes(value), token: cToken)
|
||||
: 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);
|
||||
}
|
||||
|
||||
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 Task SetDateTimeAsync(this IDistributedCache cache, string key, DateTime value, DistributedCacheEntryOptions? options = null, CancellationToken cToken = default)
|
||||
=> 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));
|
||||
}
|
||||
|
||||
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 Task SetTimeSpanAsync(this IDistributedCache cache, string key, TimeSpan value, DistributedCacheEntryOptions? options = null, CancellationToken cToken = default)
|
||||
=> 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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// create new and save
|
||||
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
|
||||
? cache.SetStringAsync(key: key, value: value, token: token)
|
||||
: cache.SetStringAsync(key: key, value: value, options: options, token: token);
|
||||
? cache.SetStringAsync(key, value, cToken)
|
||||
: cache.SetStringAsync(key, value, options, cToken);
|
||||
|
||||
if (cacheInBackground)
|
||||
_ = Task.Run(async () => await CacheAsync(), token);
|
||||
_ = Task.Run(async () => await CacheAsync(), cToken);
|
||||
else
|
||||
await CacheAsync();
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -9,14 +9,13 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using DigitalData.Core.Client;
|
||||
using EnvelopeGenerator.Application.Configurations.GtxMessaging;
|
||||
using QRCoder;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Extensions
|
||||
{
|
||||
public static class DIExtensions
|
||||
{
|
||||
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfigurationSection dispatcherConfigSection, IConfigurationSection mailConfigSection, IConfigurationSection smsConfigSection, IConfigurationSection codeGeneratorConfigSection, IConfigurationSection envelopeReceiverCacheParamsSection)
|
||||
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
//Inject CRUD Service and repositoriesad
|
||||
services.TryAddScoped<IConfigRepository, ConfigRepository>();
|
||||
@ -54,25 +53,22 @@ namespace EnvelopeGenerator.Application.Extensions
|
||||
services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly);
|
||||
services.AddAutoMapper(typeof(UserMappingProfile).Assembly);
|
||||
|
||||
services.Configure<DispatcherConfig>(dispatcherConfigSection);
|
||||
services.Configure<MailConfig>(mailConfigSection);
|
||||
services.Configure<CodeGeneratorParams>(codeGeneratorConfigSection);
|
||||
services.Configure<EnvelopeReceiverCacheParams>(envelopeReceiverCacheParamsSection);
|
||||
services.ConfigureByTypeName<DispatcherParams>(config);
|
||||
services.ConfigureByTypeName<MailParams>(config);
|
||||
services.ConfigureByTypeName<AuthenticatorParams>(config);
|
||||
services.ConfigureByTypeName<TotpSmsParams>(config);
|
||||
|
||||
services.AddHttpClientService<SmsParams>(smsConfigSection);
|
||||
services.TryAddSingleton<IMessagingService, GtxMessagingService>();
|
||||
services.TryAddSingleton<ICodeGenerator, CodeGenerator>();
|
||||
services.TryAddSingleton<IEnvelopeReceiverCache, EnvelopeReceiverCache>();
|
||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
||||
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
|
||||
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
||||
services.TryAddSingleton<QRCodeGenerator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config) => services.AddEnvelopeGenerator(
|
||||
dispatcherConfigSection: config.GetSection("DispatcherConfig"),
|
||||
mailConfigSection: config.GetSection("MailConfig"),
|
||||
smsConfigSection: config.GetSection("SmsConfig"),
|
||||
codeGeneratorConfigSection: config.GetSection("CodeGeneratorParams"),
|
||||
envelopeReceiverCacheParamsSection: config.GetSection("EnvelopeReceiverCacheParams"));
|
||||
//TODO: move to DigitalData.Core
|
||||
private static IServiceCollection ConfigureByTypeName<TOptions>(this IServiceCollection services, IConfiguration configuration) where TOptions : class
|
||||
=> services.Configure<TOptions>(configuration.GetSection(nameof(TOptions)));
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
using EnvelopeGenerator.Domain.HttpResponse;
|
||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Extensions
|
||||
{
|
||||
|
||||
@ -7,7 +7,6 @@ using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||
using EnvelopeGenerator.Application.DTOs.Receiver;
|
||||
using EnvelopeGenerator.Application.Extensions;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.HttpResponse;
|
||||
|
||||
namespace EnvelopeGenerator.Application.MappingProfiles
|
||||
{
|
||||
|
||||
@ -7,17 +7,17 @@ using System.Text;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Services
|
||||
{
|
||||
public class CodeGenerator : ICodeGenerator
|
||||
public class Authenticator : IAuthenticator
|
||||
{
|
||||
public static Lazy<CodeGenerator> LazyStatic => new(() => new CodeGenerator(Options.Create<CodeGeneratorParams>(new()), new QRCodeGenerator()));
|
||||
public static Lazy<Authenticator> LazyStatic => new(() => new Authenticator(Options.Create<AuthenticatorParams>(new()), new QRCodeGenerator()));
|
||||
|
||||
public static CodeGenerator Static => LazyStatic.Value;
|
||||
public static Authenticator Static => LazyStatic.Value;
|
||||
|
||||
private readonly CodeGeneratorParams _params;
|
||||
private readonly AuthenticatorParams _params;
|
||||
|
||||
private readonly QRCodeGenerator _qrCodeGenerator;
|
||||
|
||||
public CodeGenerator(IOptions<CodeGeneratorParams> options, QRCodeGenerator qrCodeGenerator)
|
||||
public Authenticator(IOptions<AuthenticatorParams> options, QRCodeGenerator qrCodeGenerator)
|
||||
{
|
||||
_params = options.Value;
|
||||
_qrCodeGenerator = qrCodeGenerator;
|
||||
@ -62,5 +62,10 @@ namespace EnvelopeGenerator.Application.Services
|
||||
totpUrlFormat: totpUrlFormat,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -21,19 +21,19 @@ namespace EnvelopeGenerator.Application.Services
|
||||
{
|
||||
private readonly IEmailTemplateService _tempService;
|
||||
private readonly IEnvelopeReceiverService _envRcvService;
|
||||
private readonly DispatcherConfig _dConfig;
|
||||
private readonly DispatcherParams _dConfig;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Dictionary<string, string> _placeholders;
|
||||
private readonly ICodeGenerator _codeGenerator;
|
||||
private readonly IAuthenticator _authenticator;
|
||||
|
||||
public EnvelopeMailService(IEmailOutRepository repository, IMapper mapper, IEmailTemplateService tempService, IEnvelopeReceiverService envelopeReceiverService, IOptions<DispatcherConfig> dispatcherConfigOptions, IConfigService configService, IOptions<MailConfig> mailConfig, ICodeGenerator codeGenerator) : base(repository, mapper)
|
||||
public EnvelopeMailService(IEmailOutRepository repository, IMapper mapper, IEmailTemplateService tempService, IEnvelopeReceiverService envelopeReceiverService, IOptions<DispatcherParams> dispatcherConfigOptions, IConfigService configService, IOptions<MailParams> mailConfig, IAuthenticator authenticator) : base(repository, mapper)
|
||||
{
|
||||
_tempService = tempService;
|
||||
_envRcvService = envelopeReceiverService;
|
||||
_dConfig = dispatcherConfigOptions.Value;
|
||||
_configService = configService;
|
||||
_placeholders = mailConfig.Value.Placeholders;
|
||||
_codeGenerator = codeGenerator;
|
||||
_authenticator = authenticator;
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> CreatePlaceholders(string? accessCode = null, EnvelopeReceiverDto? envelopeReceiverDto = null)
|
||||
@ -167,7 +167,7 @@ namespace EnvelopeGenerator.Application.Services
|
||||
if (dto.Receiver.TotpExpiration is null)
|
||||
throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver.TotpExpiration is null. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}");
|
||||
|
||||
var totp_qr_64 = _codeGenerator.GenerateTotpQrCode(userEmail: dto.Receiver.EmailAddress, secretKey: dto.Receiver.TotpSecretkey).ToBase64String();
|
||||
var totp_qr_64 = _authenticator.GenerateTotpQrCode(userEmail: dto.Receiver.EmailAddress, secretKey: dto.Receiver.TotpSecretkey).ToBase64String();
|
||||
return SendAsync(dto, EmailTemplateType.TotpSecret, new()
|
||||
{
|
||||
{"[TFA_QR_CODE]", totp_qr_64 },
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,13 +17,13 @@ namespace EnvelopeGenerator.Application.Services
|
||||
{
|
||||
private readonly IStringLocalizer<Resource> _localizer;
|
||||
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly ISmsSender _smsSender;
|
||||
|
||||
public EnvelopeReceiverService(IEnvelopeReceiverRepository repository, IStringLocalizer<Resource> localizer, IMapper mapper, IMessagingService messagingService)
|
||||
public EnvelopeReceiverService(IEnvelopeReceiverRepository repository, IStringLocalizer<Resource> localizer, IMapper mapper, ISmsSender smsSender)
|
||||
: base(repository, mapper)
|
||||
{
|
||||
_localizer = localizer;
|
||||
_messagingService = messagingService;
|
||||
_smsSender = smsSender;
|
||||
}
|
||||
|
||||
public async Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true, bool readOnly = true)
|
||||
@ -171,7 +171,7 @@ namespace EnvelopeGenerator.Application.Services
|
||||
.Message(Key.PhoneNumberNonexists)
|
||||
.Notice(LogLevel.Error, Flag.NotFound, $"An attempt was made to send sms to the user whose phone number is null. Envelope recipient ID is {envelopeReceiverId}, UUID is {uuid} and signature is {signature}.");
|
||||
|
||||
var res = await _messagingService.SendSmsAsync(recipient: env_rcv.PhoneNumber, message: message);
|
||||
var res = await _smsSender.SendSmsAsync(recipient: env_rcv.PhoneNumber, message: message);
|
||||
|
||||
return Result.Success(res);
|
||||
}
|
||||
|
||||
53
EnvelopeGenerator.Application/Services/EnvelopeSmsHandler.cs
Normal file
53
EnvelopeGenerator.Application/Services/EnvelopeSmsHandler.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using EnvelopeGenerator.Application.Configurations;
|
||||
using EnvelopeGenerator.Application.Contracts;
|
||||
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||
using EnvelopeGenerator.Application.Extensions;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Services;
|
||||
|
||||
public class EnvelopeSmsHandler : IEnvelopeSmsHandler
|
||||
{
|
||||
private readonly ISmsSender _sender;
|
||||
|
||||
private readonly TotpSmsParams _totpSmsParams;
|
||||
|
||||
private readonly IDistributedCache _dCache;
|
||||
|
||||
private readonly IAuthenticator _authenticator;
|
||||
|
||||
public EnvelopeSmsHandler(ISmsSender sender, IOptions<TotpSmsParams> totpSmsParamsOptions, IDistributedCache distributedCache, IAuthenticator authenticator)
|
||||
{
|
||||
_sender = sender;
|
||||
_totpSmsParams = totpSmsParamsOptions.Value;
|
||||
_dCache = distributedCache;
|
||||
_authenticator = authenticator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If expiration is passed then, sends sms and returns smsResponse and up-to-date expiration; otherwise send expiration.
|
||||
/// </summary>
|
||||
/// <param name="er_secret"></param>
|
||||
/// <param name="cToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(SmsResponse? SmsResponse, DateTime Expiration)> SendTotpAsync(EnvelopeReceiverSecretDto er_secret, CancellationToken cToken = default)
|
||||
{
|
||||
var key = string.Format(_totpSmsParams.Expiration.CacheKeyFormat, er_secret.EnvelopeId, er_secret.ReceiverId);
|
||||
var expiration = await _dCache.GetDateTimeAsync(key, cToken);
|
||||
|
||||
if(expiration is DateTime expirationDateTime && expirationDateTime < DateTime.Now)
|
||||
return (null, expirationDateTime);
|
||||
else
|
||||
{
|
||||
var new_expiration = DateTime.Now.AddSeconds(_totpSmsParams.TotpStep);
|
||||
var totp = _authenticator.GenerateTotp(er_secret.Receiver!.TotpSecretkey!, _totpSmsParams.TotpStep);
|
||||
var msg = string.Format(_totpSmsParams.Format, totp, new_expiration.ToString(_totpSmsParams.Expiration.Format, _totpSmsParams.Expiration.CultureInfo));
|
||||
return (await _sender.SendSmsAsync(er_secret.PhoneNumber!, msg), new_expiration);
|
||||
}
|
||||
}
|
||||
|
||||
public bool VerifyTotp(string totpCode, string secretKey) => _authenticator
|
||||
.VerifyTotp(totpCode, secretKey, _totpSmsParams.TotpStep, _totpSmsParams.TotpVerificationWindow);
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Client;
|
||||
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
|
||||
{
|
||||
public class GtxMessagingService : IMessagingService
|
||||
{
|
||||
private readonly IHttpClientService<SmsParams> _smsClient;
|
||||
|
||||
private readonly SmsParams _smsParams;
|
||||
|
||||
private readonly 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)
|
||||
{
|
||||
return await _smsClient.FetchAsync(queryParams: new Dictionary<string, object?>()
|
||||
{
|
||||
{ _smsParams.RecipientQueryParamName, recipient },
|
||||
{ _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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
EnvelopeGenerator.Application/Services/GTXSmsSender.cs
Normal file
40
EnvelopeGenerator.Application/Services/GTXSmsSender.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Client;
|
||||
using DigitalData.Core.Client;
|
||||
using EnvelopeGenerator.Application.Configurations;
|
||||
using EnvelopeGenerator.Application.Contracts;
|
||||
using EnvelopeGenerator.Application.DTOs.Messaging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Services;
|
||||
|
||||
//TODO: move to DigitalData.Core
|
||||
public class GTXSmsSender : ISmsSender
|
||||
{
|
||||
private readonly IHttpClientService<GtxMessagingParams> _smsClient;
|
||||
|
||||
private readonly GtxMessagingParams _smsParams;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public string ServiceProvider { get; }
|
||||
|
||||
public GTXSmsSender(IHttpClientService<GtxMessagingParams> smsClient, IOptions<GtxMessagingParams> smsParamsOptions, IMapper mapper)
|
||||
{
|
||||
_smsClient = smsClient;
|
||||
_smsParams = smsParamsOptions.Value;
|
||||
_mapper = mapper;
|
||||
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
|
||||
}
|
||||
|
||||
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
|
||||
{
|
||||
return await _smsClient.FetchAsync(queryParams: new Dictionary<string, object?>()
|
||||
{
|
||||
{ _smsParams.RecipientQueryParamName, recipient },
|
||||
{ _smsParams.MessageQueryParamName, message }
|
||||
})
|
||||
.ThenAsync(res => res.Json<GtxMessagingResponse>())
|
||||
.ThenAsync(_mapper.Map<SmsResponse>);
|
||||
}
|
||||
}
|
||||
@ -84,6 +84,9 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
[Column("EXPIRES_WARNING_WHEN_DAYS")]
|
||||
public int? ExpiresWarningWhenDays { get; set; }
|
||||
|
||||
[Column("TFA_ENABLED", TypeName = "bit")]
|
||||
public bool TFAEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sender of envelope
|
||||
/// </summary>
|
||||
|
||||
@ -46,9 +46,6 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
[RegularExpression(@"^\+[0-9]+$", ErrorMessage = "Phone number must start with '+' followed by digits.")]
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
[Column("TFA_ENABLED", TypeName = "bit")]
|
||||
public bool TFAEnabled { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public (int Envelope, int Receiver) Id => (Envelope: EnvelopeId, Receiver: ReceiverId);
|
||||
|
||||
|
||||
@ -35,12 +35,11 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
private readonly Cultures _cultures;
|
||||
private readonly IEnvelopeMailService _mailService;
|
||||
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
|
||||
private readonly IMessagingService _msgService;
|
||||
private readonly IEnvelopeReceiverCache _erCache;
|
||||
private readonly ICodeGenerator _codeGenerator;
|
||||
private readonly IAuthenticator _authenticator;
|
||||
private readonly IReceiverService _rcvService;
|
||||
private readonly IEnvelopeSmsHandler _envSmsHandler;
|
||||
|
||||
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, IAuthenticator authenticator, IReceiverService receiverService, IEnvelopeSmsHandler envelopeSmsService)
|
||||
{
|
||||
this.envelopeOldService = envelopeOldService;
|
||||
_envRcvService = envelopeReceiverService;
|
||||
@ -52,10 +51,9 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
_mailService = envelopeMailService;
|
||||
_logger = logger;
|
||||
_readOnlyService = readOnlyService;
|
||||
_msgService = messagingService;
|
||||
_erCache = envelopeReceiverCache;
|
||||
_codeGenerator = codeGenerator;
|
||||
_authenticator = authenticator;
|
||||
_rcvService = receiverService;
|
||||
_envSmsHandler = envelopeSmsService;
|
||||
}
|
||||
|
||||
[HttpGet("/")]
|
||||
@ -146,7 +144,7 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync(
|
||||
Success: er => View()
|
||||
.WithData("EnvelopeKey", envelopeReceiverId)
|
||||
.WithData("TFAEnabled", er.TFAEnabled)
|
||||
.WithData("TFAEnabled", er.Envelope!.TFAEnabled)
|
||||
.WithData("HasPhoneNumber", er.HasPhoneNumber),
|
||||
Fail: IActionResult (messages, notices) =>
|
||||
{
|
||||
@ -162,6 +160,91 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
#region TFA Views
|
||||
[NonAction]
|
||||
private async Task<IActionResult> TFAViewAsync(bool viaSms, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
|
||||
{
|
||||
if (viaSms)
|
||||
{
|
||||
var (smsRes, expiration) = await _envSmsHandler.SendTotpAsync(er_secret);
|
||||
|
||||
if (smsRes is not null && smsRes.Failed)
|
||||
{
|
||||
var res_json = JsonConvert.SerializeObject(smsRes);
|
||||
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: $"An unexpected error occurred while sending an SMS code. Response: ${res_json}");
|
||||
return this.ViewInnerServiceError();
|
||||
}
|
||||
|
||||
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", expiration);
|
||||
}
|
||||
else
|
||||
{
|
||||
return View("EnvelopeLocked").WithData("CodeType", "authenticatorCode").WithData("QRCodeExpiration", er_secret.Receiver?.TotpExpiration);
|
||||
}
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
private async Task<IActionResult?> HandleAccessCodeAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
|
||||
{
|
||||
//check the access code verification
|
||||
if (er_secret.AccessCode != auth.AccessCode)
|
||||
{
|
||||
//Constants.EnvelopeStatus.AccessCodeIncorrect
|
||||
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeIncorrect);
|
||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return View("EnvelopeLocked")
|
||||
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
|
||||
}
|
||||
|
||||
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeCorrect);
|
||||
|
||||
//check if the user has phone is added
|
||||
if (er_secret.Envelope!.TFAEnabled)
|
||||
{
|
||||
var rcv = er_secret.Receiver;
|
||||
if (rcv.IsTotpSecretInvalid())
|
||||
{
|
||||
rcv.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
|
||||
rcv.TotpExpiration = DateTime.Now.AddMonths(1);
|
||||
await _rcvService.UpdateAsync(rcv);
|
||||
await _mailService.SendTFAQrCodeAsync(er_secret);
|
||||
}
|
||||
return await TFAViewAsync(auth.UserSelectSMS, er_secret, envelopeReceiverId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
private async Task<IActionResult?> HandleSmsAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
|
||||
{
|
||||
if (er_secret.Receiver!.TotpSecretkey is null)
|
||||
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
|
||||
|
||||
if (_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey))
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
||||
return await TFAViewAsync(viaSms: true, er_secret, envelopeReceiverId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
private async Task<IActionResult?> HandleAuthenticatorAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
|
||||
{
|
||||
if (er_secret.Receiver!.IsTotpInvalid(totp: auth.AuthenticatorCode!))
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
||||
return await TFAViewAsync(viaSms: false, er_secret, envelopeReceiverId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
[HttpPost("EnvelopeKey/{envelopeReceiverId}/Locked")]
|
||||
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth)
|
||||
{
|
||||
@ -183,30 +266,14 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
//check access code
|
||||
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId);
|
||||
|
||||
return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync(
|
||||
SuccessAsync: async er_secret =>
|
||||
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||
|
||||
if (er_secret_res.IsFailed)
|
||||
{
|
||||
async Task<IActionResult> TFAView(bool viaSms)
|
||||
{
|
||||
if (viaSms)
|
||||
{
|
||||
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);
|
||||
}
|
||||
_logger.LogNotice(er_secret_res.Notices);
|
||||
return this.ViewEnvelopeNotFound();
|
||||
}
|
||||
var er_secret = er_secret_res.Data;
|
||||
|
||||
if (auth.HasMulti)
|
||||
{
|
||||
@ -215,56 +282,14 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
.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);
|
||||
}
|
||||
|
||||
}
|
||||
if(await HandleAccessCodeAsync(auth, er_secret, envelopeReceiverId) is IActionResult acView)
|
||||
return acView;
|
||||
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);
|
||||
}
|
||||
}
|
||||
if(await HandleSmsAsync(auth, er_secret, envelopeReceiverId) is IActionResult smsView)
|
||||
return smsView;
|
||||
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);
|
||||
}
|
||||
}
|
||||
if(await HandleAuthenticatorAsync(auth, er_secret, envelopeReceiverId) is IActionResult aView)
|
||||
return aView;
|
||||
else
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
@ -323,13 +348,6 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
|
||||
|
||||
return View("ShowEnvelope", er);
|
||||
},
|
||||
Fail: (messages, notices) =>
|
||||
{
|
||||
_logger.LogNotice(notices);
|
||||
return this.ViewEnvelopeNotFound();
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -384,7 +402,8 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId).ThenAsync(
|
||||
SuccessAsync: async (er) =>
|
||||
{ViewData["UserCulture"] = _cultures[UserLanguage];
|
||||
{
|
||||
ViewData["UserCulture"] = _cultures[UserLanguage];
|
||||
ViewData["UserCulture"] = _cultures[UserLanguage];
|
||||
return await _historyService.IsRejected(envelopeId: er.EnvelopeId)
|
||||
? View(er)
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
using EnvelopeGenerator.Application.Contracts;
|
||||
using EnvelopeGenerator.Application.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace EnvelopeGenerator.Web.Controllers.Test
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/test/[controller]")]
|
||||
public class TestAuthController : ControllerBase
|
||||
{
|
||||
private readonly IJWTService<string> _authService;
|
||||
|
||||
public TestAuthController(IJWTService<string> authService)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult ProvideToken([FromQuery] string value)
|
||||
{
|
||||
var token = _authService.GenerateToken(value);
|
||||
return Ok(token);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetSecurityToken([FromQuery] string token)
|
||||
{
|
||||
var sToken = _authService.ReadSecurityToken(token);
|
||||
return Ok(sToken);
|
||||
}
|
||||
|
||||
[HttpGet("Username")]
|
||||
[Authorize]
|
||||
public IActionResult Getname()
|
||||
{
|
||||
var username = User.Claims?.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||
return Ok(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,9 @@ namespace EnvelopeGenerator.Web.Controllers.Test
|
||||
[ApiController]
|
||||
public class TestMessagingController : ControllerBase
|
||||
{
|
||||
private readonly IMessagingService _service;
|
||||
private readonly ISmsSender _service;
|
||||
|
||||
public TestMessagingController(IMessagingService service)
|
||||
public TestMessagingController(ISmsSender service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PackageId>EnvelopeGenerator.Web</PackageId>
|
||||
<Version>2.8.1</Version>
|
||||
<Version>2.9.0</Version>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>EnvelopeGenerator.Web</Product>
|
||||
@ -13,8 +13,8 @@
|
||||
<PackageTags>digital data envelope generator web</PackageTags>
|
||||
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
||||
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
||||
<AssemblyVersion>2.8.1</AssemblyVersion>
|
||||
<FileVersion>2.8.1</FileVersion>
|
||||
<AssemblyVersion>2.9.0</AssemblyVersion>
|
||||
<FileVersion>2.9.0</FileVersion>
|
||||
<Copyright>Copyright © 2024 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@ -70,11 +70,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": null,
|
||||
"Audience": null,
|
||||
"Key": "8RGnd7x0G2TYLOIW4m_qlIls7MfbAIGNrpQJzMAUIvULHOLiG723znRa_MG-Z4yw3SErusOU4hTui2rVBMcCaQ"
|
||||
},
|
||||
"ContactLink": {
|
||||
"Label": "Kontakt",
|
||||
"Href": "https://digitaldata.works/",
|
||||
@ -113,30 +108,25 @@
|
||||
"ShowPageClass": "dd-show-logo",
|
||||
"LockedPageClass": "dd-locked-logo"
|
||||
},
|
||||
"DispatcherConfig": {
|
||||
"DispatcherParams": {
|
||||
"SendingProfile": 1,
|
||||
"AddedWho": "DDEnvelopGenerator",
|
||||
"ReminderTypeId": 202377,
|
||||
"EmailAttmt1": ""
|
||||
},
|
||||
"MailConfig": {
|
||||
"MailParams": {
|
||||
"Placeholders": {
|
||||
"[NAME_PORTAL]": "signFlow",
|
||||
"[SIGNATURE_TYPE]": "signieren",
|
||||
"[REASON]": ""
|
||||
}
|
||||
},
|
||||
"GTXMessagingConfig": {
|
||||
"AuthKey": "ep$?A!Gs"
|
||||
},
|
||||
"SmsConfig": {
|
||||
"GtxMessagingParams": {
|
||||
"Uri": "https://rest.gtx-messaging.net",
|
||||
"Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json",
|
||||
"Headers": {},
|
||||
"QueryParams": {
|
||||
"from": "signFlow"
|
||||
},
|
||||
"CodeCacheValidityPeriod": "00:10:00"
|
||||
},
|
||||
"EnvelopeReceiverCacheParams": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user