Compare commits

...

9 Commits

Author SHA1 Message Date
Developer 02
cf300d3ade Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2025-01-27 16:50:44 +01:00
Developer 02
be44f9f436 refactor(HomeController): Statische Eigenschaften SmsTotpStep und SmsFormat hinzugefügt. 2025-01-27 15:01:34 +01:00
Developer 02
80f9107e4e feat(cache): Unterstützung für GetOrSetAsync mit DateTime-Typ hinzugefügt
- GetOrSetAsync für DateTime mit synchronen und asynchronen Fabrikmethoden implementiert.
- Bestehende GetOrSetAsync-Methoden für Zeichenfolgen und asynchrone Zeichenfolgen refaktoriert, um Klarheit und Struktur zu verbessern.
- Code mit Regionen organisiert, um ähnliche Methoden für bessere Lesbarkeit zu gruppieren.
- TODO für weitere Verbesserungen bei der Codegenerierung für GetOrSetAsync-Methoden hinzugefügt.
2025-01-27 14:50:23 +01:00
Developer 02
c6e9ecfbca refactor(cache): Unterstützung für CancellationToken in IDistributedCache-Erweiterungsmethoden hinzufügen
- Aktualisierte `SetLongAsync`, `GetLongAsync`, `SetDateTimeAsync`, `GetDateTimeAsync`, `SetTimeSpanAsync` und `GetTimeSpanAsync`, um optionale `CancellationToken`-Parameter zu unterstützen.
- Modifizierte `GetOrSetAsync`, um zwischen synchronen und asynchronen Fabrikfunktionen zu unterscheiden.
- Sicherstellung einer konsistenten Handhabung von `CancellationToken` in allen Cache-bezogenen Operationen.
2025-01-27 14:23:06 +01:00
Developer 02
af5d7c289d refactor(HomeController): LogInEnvelope aktualisiert, um SMS-Code als TOTP zu verifizieren 2025-01-27 13:47:26 +01:00
Developer 02
3267acbeb3 feat(CodeGenerator): GenerateTotp und VerifyTotp Methoden hinzugefügt. 2025-01-25 00:35:19 +01:00
Developer 02
95efe58e1b chore(Web): Hochgestuft auf 2.8.2 2025-01-24 20:42:01 +01:00
Developer 02
867756242e refactor(EnvelopeReceiver): TFAEnabled wurde in die Envelope-Tabelle für Entität und DTO verschoben.
- Aktualisierte zugehörige Felder in HomeController.
2025-01-24 18:13:29 +01:00
Developer 02
713c2f3ed2 refactor(GTXMessagingResponse): In die Anwendungsschicht verschoben. 2025-01-24 17:11:38 +01:00
20 changed files with 376 additions and 405 deletions

View File

@@ -13,7 +13,5 @@
/// The placeholder {0} represents the envelopeReceiverId.
/// </summary>
public string CodeExpirationCacheKeyFormat { get; init; } = "sms-code-expiration-{0}";
public TimeSpan CodeCacheValidityPeriod { get; init; } = new(0, 5, 0);
}
}

View File

@@ -1,5 +1,6 @@
using DigitalData.Core.Abstractions.Client;
using Microsoft.Extensions.Caching.Distributed;
using OtpNet;
namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
{
@@ -21,5 +22,9 @@ namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
public string MessageQueryParamName { get; init; } = "text";
public int CodeLength { get; init; } = 5;
public int SmsTotpStep { get; init; } = 300;
public string DefaultTotpMessageFormat { get; init; } = "{0}";
}
}

View File

@@ -1,13 +1,21 @@
namespace EnvelopeGenerator.Application.Contracts
using OtpNet;
namespace EnvelopeGenerator.Application.Contracts
{
public interface ICodeGenerator
{
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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,13 +1,12 @@
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}");
}

View File

@@ -51,7 +51,9 @@ namespace EnvelopeGenerator.Application.DTOs
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 EnvelopeType? EnvelopeType { get; set; }

View File

@@ -27,7 +27,5 @@ namespace EnvelopeGenerator.Application.DTOs.EnvelopeReceiver
public DateTime? ChangedWhen { get; init; }
public bool HasPhoneNumber { get; init; }
public bool TFAEnabled { get; init; }
}
}

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Domain.HttpResponse
namespace EnvelopeGenerator.Application.DTOs.Messaging
{
public class GtxMessagingResponse : Dictionary<string, object?> { }
}

View File

@@ -4,16 +4,6 @@
{
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; }
}
}

View File

@@ -1,59 +1,60 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\Model.Designer.vb" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Model.Designer.vb" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Application" Version="2.0.0" />
<PackageReference Include="DigitalData.Core.Client" Version="2.0.3" />
<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="QRCoder" Version="1.6.0" />
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
<PackageReference Include="UserManager.Application" Version="2.0.0" />
<PackageReference Include="UserManager.Infrastructure" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Application" Version="2.0.0" />
<PackageReference Include="DigitalData.Core.Client" Version="2.0.3" />
<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" />
<PackageReference Include="UserManager.Infrastructure" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Extensions\EnvelopeGenerator.Extensions.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Extensions\EnvelopeGenerator.Extensions.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Model.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Model.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Model.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Model.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Model.en.resx">
<CustomToolNamespace>My.Resources</CustomToolNamespace>
<LastGenOutput>Model.en.Designer.vb</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Model.resx">
<CustomToolNamespace>My.Resources</CustomToolNamespace>
<LastGenOutput>Model.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.en-US.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Model.en.resx">
<CustomToolNamespace>My.Resources</CustomToolNamespace>
<LastGenOutput>Model.en.Designer.vb</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Model.resx">
<CustomToolNamespace>My.Resources</CustomToolNamespace>
<LastGenOutput>Model.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.en-US.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -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
}
}

View File

@@ -1,4 +1,4 @@
using EnvelopeGenerator.Domain.HttpResponse;
using EnvelopeGenerator.Application.DTOs.Messaging;
namespace EnvelopeGenerator.Application.Extensions
{

View File

@@ -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
{

View File

@@ -62,5 +62,15 @@ 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);
public bool GetTotpExpirationTime(int step = 30)
{
throw new NotImplementedException();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -4,67 +4,46 @@ 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
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;
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)
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
{
return await _smsClient.FetchAsync(queryParams: new Dictionary<string, object?>()
{
_smsClient = smsClient;
_smsParams = smsParamsOptions.Value;
_mapper = mapper;
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
_codeGen = codeGenerator;
_erCache = envelopeReceiverCache;
}
{ _smsParams.RecipientQueryParamName, recipient },
{ _smsParams.MessageQueryParamName, message }
})
.ThenAsync(res => res.Json<GtxMessagingResponse>())
.ThenAsync(_mapper.Map<SmsResponse>);
}
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 };
}
}
public async Task<SmsResponse> SendSmsCodeAsync(string recipient, string secretKey, string? messageFormat = null)
{
var code = _codeGen.GenerateTotp(secretKey, _smsParams.SmsTotpStep);
var message = string.Format(messageFormat ?? _smsParams.DefaultTotpMessageFormat, code);
return await SendSmsAsync(recipient: recipient, message: message);
}
}

View File

@@ -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>

View File

@@ -45,10 +45,7 @@ namespace EnvelopeGenerator.Domain.Entities
[StringLength(20)]
[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);

View File

@@ -24,9 +24,9 @@ using EnvelopeGenerator.Application.Extensions;
namespace EnvelopeGenerator.Web.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly EnvelopeOldService envelopeOldService;
{
private readonly ILogger<HomeController> _logger;
private readonly EnvelopeOldService envelopeOldService;
private readonly IEnvelopeReceiverService _envRcvService;
private readonly IEnvelopeHistoryService _historyService;
private readonly IStringLocalizer<Resource> _localizer;
@@ -36,11 +36,12 @@ namespace EnvelopeGenerator.Web.Controllers
private readonly IEnvelopeMailService _mailService;
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
private readonly IMessagingService _msgService;
private readonly IEnvelopeReceiverCache _erCache;
private readonly ICodeGenerator _codeGenerator;
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;
_envRcvService = envelopeReceiverService;
@@ -53,7 +54,6 @@ namespace EnvelopeGenerator.Web.Controllers
_logger = logger;
_readOnlyService = readOnlyService;
_msgService = messagingService;
_erCache = envelopeReceiverCache;
_codeGenerator = codeGenerator;
_rcvService = receiverService;
}
@@ -74,7 +74,7 @@ namespace EnvelopeGenerator.Web.Controllers
return View();
}
[HttpGet("EnvelopeKey/{envelopeReceiverId}")]
public async Task<IActionResult> MainAsync([FromRoute] string envelopeReceiverId, [FromQuery] string? culture = null)
{
@@ -146,7 +146,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) =>
{
@@ -183,153 +183,152 @@ 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)
{
_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)
{
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);
}
}
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();
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.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)
{
@@ -384,7 +383,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)
@@ -437,7 +437,7 @@ namespace EnvelopeGenerator.Web.Controllers
SuccessAsync: async er =>
{
var envelopeKey = (er.Envelope!.Uuid, er.Receiver!.Signature).EncodeEnvelopeReceiverId();
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeKey);
//TODO: implement multi-threading to history process (Task)
@@ -517,7 +517,7 @@ namespace EnvelopeGenerator.Web.Controllers
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
if (string.IsNullOrEmpty(cookieValue))
return null;
return null;
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
return culture?.Value ?? null;

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>EnvelopeGenerator.Web</PackageId>
<Version>2.8.1</Version>
<Version>2.8.2</Version>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>EnvelopeGenerator.Web</Product>