Compare commits

...

15 Commits

Author SHA1 Message Date
Developer 02
4f5b8f9d76 feat(EnvelopeReceiverService): Optionale schreibgeschützte Eingabe als Schnittstellenimplementierung hinzugefügt.
- als Standard ist Nur-Lesen wahr.
2024-12-10 22:48:43 +01:00
Developer 02
f06b41492e feat(EnvelopeReceiverRepository): Standardwert readOnly als true aktualisiert. 2024-12-10 22:43:53 +01:00
Developer 02
f0f1275e75 feat(EnvelopeReceiverRepository): Optionale schreibgeschützte Eingabe als Schnittstellenimplementierung hinzugefügt.
- Standardmäßig ist schreibgeschützt falsch.
2024-12-10 22:33:32 +01:00
Developer 02
085f37de16 feat(CodeGenerator): Die Methoden GenerateTotpSecretKey, GenerateTotpQrCode und GenerateTotpQrCode wurden als Schnittstellenimplementierung hinzugefügt. 2024-12-10 22:05:52 +01:00
Developer 02
1657a99aa6 feat(DTOExtensions): Optionale minutesBeforeExpiration Eingaben zu IsTotpSecretInvalid und IsTotpSecretValid Methoden hinzugefügt. 2024-12-10 20:34:22 +01:00
Developer 02
ff6d27df8e feat(DTOExtensions): Erstellt, um Erweiterungsmethoden für DTOs hinzuzufügen.
- IsTotpSecretExpired, IsTotpSecretInvalid und IsTotpSecretValid Erweiterungsmethoden für ReceiverReadDto hinzugefügt, um den Zustand des geheimen Schlüssels zu behandeln.
2024-12-10 20:32:09 +01:00
Developer 02
76bd1a102f fix(EnvelopedLocked): asp-for tag helper verwendet, um die Daten der UserSelectSMS Eigenschaft zu erhalten.
- nullibility und null check von UserSelectSMS entfernt, weil es für tag helper nicht akzeptabel ist
2024-12-10 20:13:26 +01:00
Developer 02
6a6da39bc4 refactor(HomeController): Aktualisiert, um zu prüfen, ob der UserSelectSMS-Wert falsch ist.
- Relevante Variablen zu EnvelopeLocked.cshtml hinzugefügt
2024-12-10 18:48:05 +01:00
Developer 02
137d8e09d4 refactor(HomeController): Aktualisiert, um zu prüfen, ob der UserSelectSMS-Status null ist. 2024-12-10 18:24:29 +01:00
Developer 02
bed51992d2 feat(Auth): Proproty mit dem Namen AuthenticatorCode für die Verwendung von Authenticators hinzugefügt.
- Getter mit dem Namen HasAuthenticatorCode hinzugefügt.
 - Aktualisierte HasMulti und HasNone Getter Methoden, die dies berücksichtigen.
2024-12-10 18:08:01 +01:00
Developer 02
a371abaabe feat(Auth): Nullbare Eigenschaft namens 'UserSelectSMS' hinzugefügt.
- Sie wird standardmäßig als null zugewiesen.
 - Die Checkbox des Formulars in Envelope.cshtml wurde userSelectSMS genannt.
2024-12-10 17:47:45 +01:00
Developer 02
90c6e87224 feat(EnvelopeLocked): Kontrollkästchen hinzugefügt, um TFA per SMS auswählen zu können oder nicht, wenn tfa aktiviert ist.
- Das Kontrollkästchen ist standardmäßig nicht aktiviert.
 - Das Kontrollkästchen ist deaktiviert, wenn der Benutzer keine Telefonnummer hat.
2024-12-10 17:26:09 +01:00
Developer 02
4af1534194 fix(Receiver): Behoben TotpExpiration Eigenschaft Column atribute name. 2024-12-10 13:15:25 +01:00
Developer 02
f39ac57009 feat(EnvelopeReceiver): TFAEnabled-Eigenschaft zu Entität und Basis-Dto hinzugefügt. 2024-12-10 12:11:58 +01:00
Developer 02
88d01e4ac7 refactor(Receiver): TotpSecretkey und TotpExpiration Eigenschaften zu Entity und DTOs hinzugefügt. 2024-12-10 11:09:25 +01:00
22 changed files with 202 additions and 80 deletions

View File

@@ -1,7 +0,0 @@
namespace EnvelopeGenerator.Application.Configurations
{
public class CodeGeneratorConfig
{
public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789";
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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