Compare commits
15 Commits
85c33eb0f8
...
4f5b8f9d76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f5b8f9d76 | ||
|
|
f06b41492e | ||
|
|
f0f1275e75 | ||
|
|
085f37de16 | ||
|
|
1657a99aa6 | ||
|
|
ff6d27df8e | ||
|
|
76bd1a102f | ||
|
|
6a6da39bc4 | ||
|
|
137d8e09d4 | ||
|
|
bed51992d2 | ||
|
|
a371abaabe | ||
|
|
90c6e87224 | ||
|
|
4af1534194 | ||
|
|
f39ac57009 | ||
|
|
88d01e4ac7 |
@@ -1,7 +0,0 @@
|
||||
namespace EnvelopeGenerator.Application.Configurations
|
||||
{
|
||||
public class CodeGeneratorConfig
|
||||
{
|
||||
public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace EnvelopeGenerator.Application.Configurations
|
||||
{
|
||||
public class CodeGeneratorParams
|
||||
{
|
||||
public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789";
|
||||
|
||||
public int DefaultTotpSecretKeyLength { get; init; } = 32;
|
||||
|
||||
public string TotpIssuer { get; init; } = "signFlow";
|
||||
|
||||
/// <summary>
|
||||
/// 0 is user email, 1 is secret key and 2 is issuer.
|
||||
/// </summary>
|
||||
public string TotpUrlFormat { get; init; } = "otpauth://totp/{0}?secret={1}&issuer={2}";
|
||||
|
||||
public int TotpQRPixelsPerModule { get; init; } = 20;
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,11 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -9,17 +9,17 @@ namespace EnvelopeGenerator.Application.Contracts
|
||||
public interface IEnvelopeReceiverService : IBasicCRUDService<EnvelopeReceiverDto, EnvelopeReceiver, (int Envelope, int Receiver)>
|
||||
{
|
||||
|
||||
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false);
|
||||
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false, bool readOnly = true);
|
||||
|
||||
Task<DataResult<IEnumerable<string?>>> ReadAccessCodeByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true);
|
||||
|
||||
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true);
|
||||
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true, bool readOnly = true);
|
||||
|
||||
Task<DataResult<EnvelopeReceiverDto>> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true);
|
||||
Task<DataResult<EnvelopeReceiverDto>> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true);
|
||||
|
||||
Task<DataResult<EnvelopeReceiverSecretDto>> ReadWithSecretByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true);
|
||||
Task<DataResult<EnvelopeReceiverSecretDto>> ReadWithSecretByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true);
|
||||
|
||||
Task<DataResult<EnvelopeReceiverDto>> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true);
|
||||
Task<DataResult<EnvelopeReceiverDto>> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true);
|
||||
|
||||
Task<DataResult<string>> ReadAccessCodeByIdAsync(int envelopeId, int receiverId);
|
||||
|
||||
|
||||
@@ -27,5 +27,7 @@ namespace EnvelopeGenerator.Application.DTOs.EnvelopeReceiver
|
||||
public DateTime? ChangedWhen { get; init; }
|
||||
|
||||
public bool HasPhoneNumber { get; init; }
|
||||
|
||||
public bool TFAEnabled { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using System.Text;
|
||||
|
||||
namespace EnvelopeGenerator.Application.DTOs.Receiver
|
||||
{
|
||||
public record ReceiverCreateDto([EmailAddress] string EmailAddress)
|
||||
public record ReceiverCreateDto([EmailAddress] string EmailAddress, string? TotpSecretkey = null, DateTime? TotpExpiration = null)
|
||||
{
|
||||
public string Signature => sha256HexOfMail.Value;
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace EnvelopeGenerator.Application.DTOs.Receiver
|
||||
int Id,
|
||||
string EmailAddress,
|
||||
string Signature,
|
||||
DateTime AddedWhen
|
||||
DateTime AddedWhen,
|
||||
string? TotpSecretkey = null,
|
||||
DateTime? TotpExpiration = null
|
||||
) : BaseDTO<int>(Id)
|
||||
{
|
||||
[JsonIgnore]
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
namespace EnvelopeGenerator.Application.DTOs.Receiver
|
||||
{
|
||||
public record ReceiverUpdateDto(int Id) : IUnique<int>;
|
||||
}
|
||||
public record ReceiverUpdateDto(int Id, string? TotpSecretkey = null, DateTime? TotpExpiration = null) : IUnique<int>;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using DigitalData.Core.Client;
|
||||
using EnvelopeGenerator.Application.Configurations.GtxMessaging;
|
||||
using QRCoder;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Extensions
|
||||
{
|
||||
@@ -55,13 +56,14 @@ namespace EnvelopeGenerator.Application.Extensions
|
||||
|
||||
services.Configure<DispatcherConfig>(dispatcherConfigSection);
|
||||
services.Configure<MailConfig>(mailConfigSection);
|
||||
services.Configure<CodeGeneratorConfig>(codeGeneratorConfigSection);
|
||||
services.Configure<CodeGeneratorParams>(codeGeneratorConfigSection);
|
||||
services.Configure<EnvelopeReceiverCacheParams>(envelopeReceiverCacheParamsSection);
|
||||
|
||||
services.AddHttpClientService<SmsParams>(smsConfigSection);
|
||||
services.TryAddSingleton<IMessagingService, GtxMessagingService>();
|
||||
services.TryAddSingleton<ICodeGenerator, CodeGenerator>();
|
||||
services.TryAddSingleton<IEnvelopeReceiverCache, EnvelopeReceiverCache>();
|
||||
services.TryAddSingleton<QRCodeGenerator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -70,7 +72,7 @@ namespace EnvelopeGenerator.Application.Extensions
|
||||
dispatcherConfigSection: config.GetSection("DispatcherConfig"),
|
||||
mailConfigSection: config.GetSection("MailConfig"),
|
||||
smsConfigSection: config.GetSection("SmsConfig"),
|
||||
codeGeneratorConfigSection: config.GetSection("CodeGeneratorConfig"),
|
||||
codeGeneratorConfigSection: config.GetSection("CodeGeneratorParams"),
|
||||
envelopeReceiverCacheParamsSection: config.GetSection("EnvelopeReceiverCacheParams"));
|
||||
}
|
||||
}
|
||||
16
EnvelopeGenerator.Application/Extensions/DTOExtensions.cs
Normal file
16
EnvelopeGenerator.Application/Extensions/DTOExtensions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using EnvelopeGenerator.Application.DTOs.Receiver;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Extensions
|
||||
{
|
||||
public static class DTOExtensions
|
||||
{
|
||||
public static bool IsTotpSecretExpired(this ReceiverReadDto dto, int minutesBeforeExpiration = 30)
|
||||
=> dto.TotpExpiration < DateTime.Now.AddMinutes(minutesBeforeExpiration * -1);
|
||||
|
||||
public static bool IsTotpSecretInvalid(this ReceiverReadDto dto, int minutesBeforeExpiration = 30)
|
||||
=> dto.IsTotpSecretExpired(minutesBeforeExpiration) || dto.TotpSecretkey is null;
|
||||
|
||||
public static bool IsTotpSecretValid(this ReceiverReadDto dto, int minutesBeforeExpiration = 30)
|
||||
=> !dto.IsTotpSecretInvalid(minutesBeforeExpiration);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Domain.HttpResponse;
|
||||
using EnvelopeGenerator.Domain.HttpResponse;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Extensions
|
||||
{
|
||||
@@ -8,5 +7,8 @@ namespace EnvelopeGenerator.Application.Extensions
|
||||
public static bool Ok(this GtxMessagingResponse gtxMessagingResponse)
|
||||
=> gtxMessagingResponse.TryGetValue("message-status", out var status)
|
||||
&& status?.ToString()?.ToLower() == "ok";
|
||||
|
||||
public static string ToBase64String(this byte[] bytes)
|
||||
=> Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
using EnvelopeGenerator.Application.Configurations;
|
||||
using EnvelopeGenerator.Application.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OtpNet;
|
||||
using QRCoder;
|
||||
using System.Text;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Services
|
||||
{
|
||||
public class CodeGenerator : ICodeGenerator
|
||||
{
|
||||
public static Lazy<CodeGenerator> LazyStatic => new(() => new CodeGenerator(Options.Create<CodeGeneratorConfig>(new())));
|
||||
public static Lazy<CodeGenerator> LazyStatic => new(() => new CodeGenerator(Options.Create<CodeGeneratorParams>(new()), new QRCodeGenerator()));
|
||||
|
||||
public static CodeGenerator Static => LazyStatic.Value;
|
||||
|
||||
private readonly string _charPool;
|
||||
private readonly CodeGeneratorParams _params;
|
||||
|
||||
public CodeGenerator(IOptions<CodeGeneratorConfig> options)
|
||||
private readonly QRCodeGenerator _qrCodeGenerator;
|
||||
|
||||
public CodeGenerator(IOptions<CodeGeneratorParams> options, QRCodeGenerator qrCodeGenerator)
|
||||
{
|
||||
_charPool = options.Value.CharPool;
|
||||
_params = options.Value;
|
||||
_qrCodeGenerator = qrCodeGenerator;
|
||||
}
|
||||
|
||||
public string GenerateCode(int length)
|
||||
@@ -29,9 +34,33 @@ namespace EnvelopeGenerator.Application.Services
|
||||
var passwordBuilder = new StringBuilder(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
passwordBuilder.Append(_charPool[random.Next(_charPool.Length)]);
|
||||
passwordBuilder.Append(_params.CharPool[random.Next(_params.CharPool.Length)]);
|
||||
|
||||
return passwordBuilder.ToString();
|
||||
}
|
||||
|
||||
public string GenerateTotpSecretKey(int? length = null)
|
||||
=> Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(length ?? _params.DefaultTotpSecretKeyLength));
|
||||
|
||||
public byte[] GenerateTotpQrCode(string userEmail, string secretKey, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null)
|
||||
{
|
||||
var url = string.Format(totpUrlFormat ?? _params.TotpUrlFormat,
|
||||
Uri.EscapeDataString(userEmail),
|
||||
Uri.EscapeDataString(secretKey),
|
||||
Uri.EscapeDataString(issuer ?? _params.TotpIssuer));
|
||||
using var qrCodeData = _qrCodeGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
|
||||
using var qrCode = new BitmapByteQRCode(qrCodeData);
|
||||
return qrCode.GetGraphic(pixelsPerModule ?? _params.TotpQRPixelsPerModule);
|
||||
}
|
||||
|
||||
public byte[] GenerateTotpQrCode(string userEmail, int? length = null, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null)
|
||||
{
|
||||
return GenerateTotpQrCode(
|
||||
userEmail: userEmail,
|
||||
secretKey: GenerateTotpSecretKey(length: length),
|
||||
issuer: issuer,
|
||||
totpUrlFormat: totpUrlFormat,
|
||||
pixelsPerModule: pixelsPerModule);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,15 +26,15 @@ namespace EnvelopeGenerator.Application.Services
|
||||
_messagingService = messagingService;
|
||||
}
|
||||
|
||||
public async Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true)
|
||||
public async Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true, bool readOnly = true)
|
||||
{
|
||||
var env_rcvs = await _repository.ReadBySignatureAsync(signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver);
|
||||
var env_rcvs = await _repository.ReadBySignatureAsync(signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly);
|
||||
return Result.Success(_mapper.Map<IEnumerable<EnvelopeReceiverDto>>(env_rcvs));
|
||||
}
|
||||
|
||||
public async Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false)
|
||||
public async Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false, bool readOnly = true)
|
||||
{
|
||||
var env_rcvs = await _repository.ReadByUuidAsync(uuid: uuid, withEnvelope: withEnvelope, withReceiver: withReceiver);
|
||||
var env_rcvs = await _repository.ReadByUuidAsync(uuid: uuid, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly);
|
||||
return Result.Success(_mapper.Map<IEnumerable<EnvelopeReceiverDto>>(env_rcvs));
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace EnvelopeGenerator.Application.Services
|
||||
return Result.Success(env_rcvs.Select(er => er.AccessCode));
|
||||
}
|
||||
|
||||
public async Task<DataResult<EnvelopeReceiverDto>> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true)
|
||||
public async Task<DataResult<EnvelopeReceiverDto>> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true)
|
||||
{
|
||||
var env_rcv = await _repository.ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver);
|
||||
var env_rcv = await _repository.ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly);
|
||||
if (env_rcv is null)
|
||||
return Result.Fail<EnvelopeReceiverDto>()
|
||||
.Message(Key.EnvelopeReceiverNotFound);
|
||||
@@ -54,9 +54,9 @@ namespace EnvelopeGenerator.Application.Services
|
||||
return Result.Success(_mapper.Map<EnvelopeReceiverDto>(env_rcv));
|
||||
}
|
||||
|
||||
public async Task<DataResult<EnvelopeReceiverSecretDto>> ReadWithSecretByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true)
|
||||
public async Task<DataResult<EnvelopeReceiverSecretDto>> ReadWithSecretByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true)
|
||||
{
|
||||
var env_rcv = await _repository.ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver);
|
||||
var env_rcv = await _repository.ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly);
|
||||
if (env_rcv is null)
|
||||
return Result.Fail<EnvelopeReceiverSecretDto>()
|
||||
.Message(Key.EnvelopeReceiverNotFound);
|
||||
@@ -64,7 +64,7 @@ namespace EnvelopeGenerator.Application.Services
|
||||
return Result.Success(_mapper.Map<EnvelopeReceiverSecretDto>(env_rcv));
|
||||
}
|
||||
|
||||
public async Task<DataResult<EnvelopeReceiverDto>> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true)
|
||||
public async Task<DataResult<EnvelopeReceiverDto>> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true)
|
||||
{
|
||||
(string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace EnvelopeGenerator.Application.Services
|
||||
.Notice(LogLevel.Warning, EnvelopeFlag.WrongEnvelopeReceiverId)
|
||||
.Notice(LogLevel.Warning, Flag.PossibleSecurityBreach);
|
||||
|
||||
return await ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver);
|
||||
return await ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly);
|
||||
}
|
||||
|
||||
public async Task<DataResult<bool>> VerifyAccessCodeAsync(string uuid, string signature, string accessCode)
|
||||
|
||||
@@ -46,6 +46,9 @@ 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);
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
[Column("ADDED_WHEN", TypeName = "datetime")]
|
||||
public DateTime AddedWhen { get; set; }
|
||||
|
||||
[Column("TOTP_SECRET_KEY", TypeName = "nvarchar(MAX)")]
|
||||
public string? TotpSecretkey { get; set; }
|
||||
|
||||
[Column("TOTP_EXPIRATION", TypeName = "datetime")]
|
||||
public DateTime? TotpExpiration { get; set; }
|
||||
|
||||
public IEnumerable<EnvelopeReceiver>? EnvelopeReceivers { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,19 @@ namespace EnvelopeGenerator.Infrastructure.Contracts
|
||||
{
|
||||
public interface IEnvelopeReceiverRepository : ICRUDRepository<EnvelopeReceiver, (int Envelope, int Receiver)>
|
||||
{
|
||||
Task<IEnumerable<EnvelopeReceiver>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false);
|
||||
Task<IEnumerable<EnvelopeReceiver>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false, bool readOnly = true);
|
||||
|
||||
Task<IEnumerable<EnvelopeReceiver>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true);
|
||||
Task<IEnumerable<EnvelopeReceiver>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true, bool readOnly = true);
|
||||
|
||||
Task<EnvelopeReceiver?> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true);
|
||||
Task<EnvelopeReceiver?> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true);
|
||||
|
||||
Task<string?> ReadAccessCodeAsync(string uuid, string signature);
|
||||
Task<string?> ReadAccessCodeAsync(string uuid, string signature, bool readOnly = true);
|
||||
|
||||
Task<int> CountAsync(string uuid, string signature);
|
||||
|
||||
Task<EnvelopeReceiver?> ReadByIdAsync(int envelopeId, int receiverId);
|
||||
Task<EnvelopeReceiver?> ReadByIdAsync(int envelopeId, int receiverId, bool readOnly = true);
|
||||
|
||||
Task<string?> ReadAccessCodeByIdAsync(int envelopeId, int receiverId);
|
||||
Task<string?> ReadAccessCodeByIdAsync(int envelopeId, int receiverId, bool readOnly = true);
|
||||
|
||||
Task<IEnumerable<EnvelopeReceiver>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses);
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ namespace EnvelopeGenerator.Infrastructure.Repositories
|
||||
{
|
||||
}
|
||||
|
||||
private IQueryable<EnvelopeReceiver> ReadWhere(string? uuid = null, string? signature = null, bool withEnvelope = false, bool withReceiver = false)
|
||||
private IQueryable<EnvelopeReceiver> ReadWhere(string? uuid = null, string? signature = null, bool withEnvelope = false, bool withReceiver = false, bool readOnly = true)
|
||||
{
|
||||
var query = _dbSet.AsNoTracking();
|
||||
var query = readOnly ? _dbSet.AsNoTracking() : _dbSet;
|
||||
|
||||
if (uuid is not null)
|
||||
query = query.Where(er => er.Envelope != null && er.Envelope.Uuid == uuid);
|
||||
@@ -32,31 +32,34 @@ namespace EnvelopeGenerator.Infrastructure.Repositories
|
||||
return query;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EnvelopeReceiver>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false)
|
||||
=> await ReadWhere(uuid: uuid, withEnvelope: withEnvelope, withReceiver: withReceiver).ToListAsync();
|
||||
public async Task<IEnumerable<EnvelopeReceiver>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false, bool readOnly = true)
|
||||
=> await ReadWhere(uuid: uuid, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly).ToListAsync();
|
||||
|
||||
public async Task<IEnumerable<EnvelopeReceiver>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true)
|
||||
=> await ReadWhere(signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver).ToListAsync();
|
||||
public async Task<IEnumerable<EnvelopeReceiver>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true, bool readOnly = true)
|
||||
=> await ReadWhere(signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly).ToListAsync();
|
||||
|
||||
public async Task<EnvelopeReceiver?> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true)
|
||||
=> await ReadWhere(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver).FirstOrDefaultAsync();
|
||||
public async Task<EnvelopeReceiver?> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true)
|
||||
=> await ReadWhere(uuid: uuid, signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver, readOnly: readOnly).FirstOrDefaultAsync();
|
||||
|
||||
public async Task<string?> ReadAccessCodeAsync(string uuid, string signature)
|
||||
=> await ReadWhere(uuid: uuid, signature: signature)
|
||||
public async Task<string?> ReadAccessCodeAsync(string uuid, string signature, bool readOnly = true)
|
||||
=> await ReadWhere(uuid: uuid, signature: signature, readOnly: readOnly)
|
||||
.Select(er => er.AccessCode)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
public async Task<int> CountAsync(string uuid, string signature) => await ReadWhere(uuid: uuid, signature: signature).CountAsync();
|
||||
|
||||
public IQueryable<EnvelopeReceiver> ReadById(int envelopeId, int receiverId) => _dbSet.AsNoTracking()
|
||||
.Where(er => er.EnvelopeId == envelopeId && er.ReceiverId == receiverId);
|
||||
private IQueryable<EnvelopeReceiver> ReadById(int envelopeId, int receiverId, bool readOnly = true)
|
||||
{
|
||||
var query = readOnly ? _dbSet.AsNoTracking() : _dbSet;
|
||||
return query.Where(er => er.EnvelopeId == envelopeId && er.ReceiverId == receiverId);
|
||||
}
|
||||
|
||||
public async Task<EnvelopeReceiver?> ReadByIdAsync(int envelopeId, int receiverId)
|
||||
=> await ReadById(envelopeId: envelopeId, receiverId: receiverId)
|
||||
public async Task<EnvelopeReceiver?> ReadByIdAsync(int envelopeId, int receiverId, bool readOnly = true)
|
||||
=> await ReadById(envelopeId: envelopeId, receiverId: receiverId, readOnly: readOnly)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
public async Task<string?> ReadAccessCodeByIdAsync(int envelopeId, int receiverId)
|
||||
=> await ReadById(envelopeId: envelopeId, receiverId: receiverId)
|
||||
public async Task<string?> ReadAccessCodeByIdAsync(int envelopeId, int receiverId, bool readOnly = true)
|
||||
=> await ReadById(envelopeId: envelopeId, receiverId: receiverId, readOnly: readOnly)
|
||||
.Select(er => er.AccessCode)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ using Ganss.Xss;
|
||||
using Newtonsoft.Json;
|
||||
using EnvelopeGenerator.Application.DTOs;
|
||||
using DigitalData.Core.Client;
|
||||
using DevExpress.Utils.About;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Web.Controllers
|
||||
{
|
||||
@@ -140,7 +140,10 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
ViewData["UserCulture"] = _cultures[UserLanguage];
|
||||
|
||||
return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync(
|
||||
Success: er => View().WithData("EnvelopeKey", envelopeReceiverId),
|
||||
Success: er => View()
|
||||
.WithData("EnvelopeKey", envelopeReceiverId)
|
||||
.WithData("TFAEnabled", er.TFAEnabled)
|
||||
.WithData("HasPhoneNumber", er.HasPhoneNumber),
|
||||
Fail: IActionResult (messages, notices) =>
|
||||
{
|
||||
_logger.LogNotice(notices);
|
||||
@@ -176,21 +179,28 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
//check access code
|
||||
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId);
|
||||
|
||||
return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync<EnvelopeReceiverSecretDto, IActionResult>(
|
||||
return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync(
|
||||
SuccessAsync: async er_secret =>
|
||||
{
|
||||
async Task<IActionResult> SendSmsView()
|
||||
async Task<IActionResult> TFAView(bool viaSms)
|
||||
{
|
||||
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, envelopeReceiverId: envelopeReceiverId);
|
||||
if (res.Ok)
|
||||
return View("EnvelopeLocked").WithData("ViaSms", true).WithData("Expiration", res.Expiration);
|
||||
else if (!res.Allowed)
|
||||
return View("EnvelopeLocked").WithData("ViaSms", true).WithData("Expiration", res.AllowedAt);
|
||||
if (viaSms)
|
||||
{
|
||||
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, envelopeReceiverId: envelopeReceiverId);
|
||||
if (res.Ok)
|
||||
return View("EnvelopeLocked").WithData("AccessCodeName", "smsCode").WithData("Expiration", res.Expiration);
|
||||
else if (!res.Allowed)
|
||||
return View("EnvelopeLocked").WithData("AccessCodeName", "smsCode").WithData("Expiration", res.AllowedAt);
|
||||
else
|
||||
{
|
||||
var res_json = JsonConvert.SerializeObject(res);
|
||||
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: $"An unexpected error occurred while sending an SMS code. Response: ${res_json}");
|
||||
return this.ViewInnerServiceError();
|
||||
}
|
||||
}
|
||||
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();
|
||||
return View("EnvelopeLocked").WithData("AccessCodeName", "authenticatorCode");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,10 +225,8 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, Constants.EnvelopeStatus.AccessCodeCorrect);
|
||||
|
||||
//check if the user has phone is added
|
||||
if (er_secret.HasPhoneNumber)
|
||||
{
|
||||
return await SendSmsView();
|
||||
}
|
||||
if (er_secret.TFAEnabled)
|
||||
return await TFAView(auth.UserSelectSMS);
|
||||
}
|
||||
else if (auth.HasSmsCode)
|
||||
{
|
||||
@@ -230,7 +238,7 @@ namespace EnvelopeGenerator.Web.Controllers
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
|
||||
return await SendSmsView();
|
||||
return await TFAView(viaSms: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
namespace EnvelopeGenerator.Web.Models
|
||||
{
|
||||
public record Auth(string? AccessCode = null, string? SmsCode = null)
|
||||
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||
{
|
||||
public bool HasAccessCode => AccessCode is not null;
|
||||
|
||||
public bool HasSmsCode => SmsCode is not null;
|
||||
|
||||
public bool HasMulti => HasAccessCode && HasSmsCode;
|
||||
public bool HasAuthenticatorCode => AuthenticatorCode is not null;
|
||||
|
||||
public bool HasNone => !(HasAccessCode || HasSmsCode);
|
||||
public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1;
|
||||
|
||||
public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
@using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
|
||||
@using Newtonsoft.Json
|
||||
@model Auth;
|
||||
@{
|
||||
var nonce = _accessor.HttpContext?.Items["csp-nonce"] as string;
|
||||
var logo = _logoOpt.Value;
|
||||
ViewData["Title"] = _localizer[WebKey.DocProtected];
|
||||
var userCulture = ViewData["UserCulture"] as Culture;
|
||||
bool viaSms = ViewData["ViaSms"] is bool _viaSms && _viaSms;
|
||||
var accessCodeName = viaSms ? "smsCode" : "accessCode";
|
||||
string accessCodeName = ViewData["AccessCodeName"] is string _accessCodeName ? _accessCodeName : "accessCode";
|
||||
string codePropName = char.ToUpper(accessCodeName[0]) + accessCodeName.Substring(1);
|
||||
bool viaSms = accessCodeName == "smsCode";
|
||||
bool viaAuthenticator = accessCodeName == "authenticatorCode";
|
||||
bool viaTFA = viaSms || viaAuthenticator;
|
||||
DateTime? expiration = ViewData["Expiration"] is DateTime _expiration ? _expiration : null;
|
||||
bool tfaEnabled = ViewData["TFAEnabled"] is bool _tfaEnabled && _tfaEnabled;
|
||||
bool hasPhoneNumber = ViewData["HasPhoneNumber"] is bool _hasPhoneNumber && _hasPhoneNumber;
|
||||
}
|
||||
<div class="page container py-4 px-4">
|
||||
<header class="text-center">
|
||||
@@ -37,6 +43,20 @@
|
||||
login
|
||||
</span>
|
||||
</button>
|
||||
@if (tfaEnabled)
|
||||
{
|
||||
<div class="form-check form-switch tfa-sms">
|
||||
@if(hasPhoneNumber)
|
||||
{
|
||||
<input asp-for="UserSelectSMS" class="form-check-input" name="userSelectSMS" type="checkbox" role="switch" id="flexSwitchCheckChecked">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input asp-for="UserSelectSMS" class="form-check-input" name="userSelectSMS" type="checkbox" role="switch" id="flexSwitchCheckChecked" disabled)>
|
||||
}
|
||||
<label class="form-check-label" for="flexSwitchCheckChecked">2FA per SMS</label>
|
||||
</div>
|
||||
}
|
||||
@if (expiration is not null)
|
||||
{
|
||||
<div id="sms-timer" class="alert alert-primary" role="alert">00:00</div>
|
||||
|
||||
@@ -448,6 +448,16 @@ footer#page-footer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-check.tfa-sms {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.form-check.tfa-sms .form-check-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-left: -.1rem;
|
||||
}
|
||||
|
||||
/*.flag-dropdown button {
|
||||
height: 100%;
|
||||
}*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user