diff --git a/EnvelopeGenerator.Application/Configurations/CodeGeneratorParams.cs b/EnvelopeGenerator.Application/Configurations/CodeGeneratorParams.cs new file mode 100644 index 00000000..2ba65399 --- /dev/null +++ b/EnvelopeGenerator.Application/Configurations/CodeGeneratorParams.cs @@ -0,0 +1,19 @@ +namespace EnvelopeGenerator.Application.Configurations +{ + public class CodeGeneratorParams + { + public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789"; + + //TODO: Increase the DefaultTotpSecretKeyLength (e.g. to 32) but make sure that the QR code is generated correctly and can be scanned by the authenticator. + public int DefaultTotpSecretKeyLength { get; init; } = 20; + + public string TotpIssuer { get; init; } = "signFlow"; + + /// + /// 0 is user email, 1 is secret key and 2 is issuer. + /// + public string TotpUrlFormat { get; init; } = "otpauth://totp/{0}?secret={1}&issuer={2}"; + + public int TotpQRPixelsPerModule { get; init; } = 20; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Configurations/EnvelopeReceiverCacheParams.cs b/EnvelopeGenerator.Application/Configurations/EnvelopeReceiverCacheParams.cs new file mode 100644 index 00000000..b8ab323e --- /dev/null +++ b/EnvelopeGenerator.Application/Configurations/EnvelopeReceiverCacheParams.cs @@ -0,0 +1,19 @@ +namespace EnvelopeGenerator.Application.Configurations +{ + public class EnvelopeReceiverCacheParams + { + /// + /// Gets the cache key format for SMS codes. + /// The placeholder {0} represents the envelopeReceiverId. + /// + public string CodeCacheKeyFormat { get; init; } = "sms-code-{0}"; + + /// + /// Gets the cache expiration key format for SMS codes. + /// The placeholder {0} represents the envelopeReceiverId. + /// + public string CodeExpirationCacheKeyFormat { get; init; } = "sms-code-expiration-{0}"; + + public TimeSpan CodeCacheValidityPeriod { get; init; } = new(0, 5, 0); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Configurations/GtxMessaging/SmsParams.cs b/EnvelopeGenerator.Application/Configurations/GtxMessaging/SmsParams.cs new file mode 100644 index 00000000..7cea8047 --- /dev/null +++ b/EnvelopeGenerator.Application/Configurations/GtxMessaging/SmsParams.cs @@ -0,0 +1,25 @@ +using DigitalData.Core.Abstractions.Client; +using Microsoft.Extensions.Caching.Distributed; + +namespace EnvelopeGenerator.Application.Configurations.GtxMessaging +{ + /// + /// https://www.gtx-messaging.com/en/api-docs/sms-rest-api/ + /// + public class SmsParams : IHttpClientOptions + { + public required string Uri { get; init; } + + public string? Path { get; init; } + + public Dictionary? Headers { get; init; } + + public Dictionary? QueryParams { get; init; } + + public string RecipientQueryParamName { get; init; } = "to"; + + public string MessageQueryParamName { get; init; } = "text"; + + public int CodeLength { get; init; } = 5; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/ICodeGenerator.cs b/EnvelopeGenerator.Application/Contracts/ICodeGenerator.cs new file mode 100644 index 00000000..38a27b85 --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/ICodeGenerator.cs @@ -0,0 +1,13 @@ +namespace EnvelopeGenerator.Application.Contracts +{ + public interface ICodeGenerator + { + string GenerateCode(int length); + + public string GenerateTotpSecretKey(int? length = null); + + public byte[] GenerateTotpQrCode(string userEmail, string secretKey, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null); + + public byte[] GenerateTotpQrCode(string userEmail, int? length = null, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/IEnvelopeMailService.cs b/EnvelopeGenerator.Application/Contracts/IEnvelopeMailService.cs index 06a4ab18..14d94057 100644 --- a/EnvelopeGenerator.Application/Contracts/IEnvelopeMailService.cs +++ b/EnvelopeGenerator.Application/Contracts/IEnvelopeMailService.cs @@ -8,10 +8,12 @@ namespace EnvelopeGenerator.Application.Contracts { public interface IEnvelopeMailService : IEmailOutService { - Task> SendAsync(EnvelopeReceiverDto envelopeReceiverDto, Constants.EmailTemplateType tempType); + Task> SendAsync(EnvelopeReceiverDto envelopeReceiverDto, Constants.EmailTemplateType tempType, Dictionary? optionalPlaceholders = null); - Task> SendAsync(EnvelopeReceiverReadOnlyDto dto); + Task> SendAsync(EnvelopeReceiverReadOnlyDto dto, Dictionary? optionalPlaceholders = null); Task> SendAccessCodeAsync(EnvelopeReceiverDto envelopeReceiverDto); + + Task> SendTFAQrCodeAsync(EnvelopeReceiverDto envelopeReceiverDto); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/IEnvelopeReceiverCache.cs b/EnvelopeGenerator.Application/Contracts/IEnvelopeReceiverCache.cs new file mode 100644 index 00000000..e2ac2a7c --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/IEnvelopeReceiverCache.cs @@ -0,0 +1,17 @@ +namespace EnvelopeGenerator.Application.Contracts +{ + public interface IEnvelopeReceiverCache + { + Task GetSmsCodeAsync(string envelopeReceiverId); + + /// + /// Asynchronously stores an SMS verification code in the cache and returns the expiration date of the code. + /// + /// The unique identifier for the recipient of the envelope to associate with the SMS code. + /// The SMS verification code to be stored. + /// A task that represents the asynchronous operation. The task result contains the expiration date and time of the stored SMS code. + Task SetSmsCodeAsync(string envelopeReceiverId, string code); + + Task GetSmsCodeExpirationAsync(string envelopeReceiverId); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/IEnvelopeReceiverService.cs b/EnvelopeGenerator.Application/Contracts/IEnvelopeReceiverService.cs index 75a897a2..f22fde28 100644 --- a/EnvelopeGenerator.Application/Contracts/IEnvelopeReceiverService.cs +++ b/EnvelopeGenerator.Application/Contracts/IEnvelopeReceiverService.cs @@ -1,7 +1,7 @@ using DigitalData.Core.Abstractions.Application; using DigitalData.Core.DTO; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; -using EnvelopeGenerator.Application.DTOs.Receiver; +using EnvelopeGenerator.Application.DTOs.Messaging; using EnvelopeGenerator.Domain.Entities; namespace EnvelopeGenerator.Application.Contracts @@ -9,15 +9,17 @@ namespace EnvelopeGenerator.Application.Contracts public interface IEnvelopeReceiverService : IBasicCRUDService { - Task>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false); + Task>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false, bool readOnly = true); - Task>> ReadSecretByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true); + Task>> ReadAccessCodeByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true); - Task>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true); + Task>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true, bool readOnly = true); - Task> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true); + Task> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true); - Task> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true); + Task> ReadWithSecretByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true); + + Task> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true); Task> ReadAccessCodeByIdAsync(int envelopeId, int receiverId); @@ -30,5 +32,7 @@ namespace EnvelopeGenerator.Application.Contracts Task>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses); Task> ReadLastUsedReceiverNameByMail(string mail); + + Task> SendSmsAsync(string envelopeReceiverId, string message); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/IMessagingService.cs b/EnvelopeGenerator.Application/Contracts/IMessagingService.cs new file mode 100644 index 00000000..5940e50d --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/IMessagingService.cs @@ -0,0 +1,13 @@ +using EnvelopeGenerator.Application.DTOs.Messaging; + +namespace EnvelopeGenerator.Application.Contracts +{ + public interface IMessagingService + { + string ServiceProvider { get; } + + Task SendSmsAsync(string recipient, string message); + + Task SendSmsCodeAsync(string recipient, string envelopeReceiverId); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/IReceiverService.cs b/EnvelopeGenerator.Application/Contracts/IReceiverService.cs index 3c08f17b..c474363b 100644 --- a/EnvelopeGenerator.Application/Contracts/IReceiverService.cs +++ b/EnvelopeGenerator.Application/Contracts/IReceiverService.cs @@ -1,4 +1,5 @@ -using DigitalData.Core.Abstractions.Application; +using DigitalData.Core.Abstractions; +using DigitalData.Core.Abstractions.Application; using DigitalData.Core.DTO; using EnvelopeGenerator.Application.DTOs.Receiver; using EnvelopeGenerator.Domain.Entities; @@ -7,8 +8,10 @@ namespace EnvelopeGenerator.Application.Contracts { public interface IReceiverService : ICRUDService { - public Task> ReadByAsync(string? emailAddress = null, string? signature = null); + Task> ReadByAsync(string? emailAddress = null, string? signature = null); - public Task DeleteByAsync(string? emailAddress = null, string? signature = null); + Task DeleteByAsync(string? emailAddress = null, string? signature = null); + + Task UpdateAsync(TUpdateDto updateDto) where TUpdateDto : IUnique; } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DIExtensions.cs b/EnvelopeGenerator.Application/DIExtensions.cs deleted file mode 100644 index c8179fa6..00000000 --- a/EnvelopeGenerator.Application/DIExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using DigitalData.UserManager.Application.MappingProfiles; -using EnvelopeGenerator.Application.Contracts; -using EnvelopeGenerator.Application.MappingProfiles; -using EnvelopeGenerator.Application.Configurations; -using EnvelopeGenerator.Application.Services; -using EnvelopeGenerator.Infrastructure.Contracts; -using EnvelopeGenerator.Infrastructure.Repositories; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace EnvelopeGenerator.Application -{ - public static class DIExtensions - { - public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration dispatcherConfigSection, IConfiguration mailConfigSection) - { - //Inject CRUD Service and repositoriesad - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - //Auto mapping profiles - services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly); - services.AddAutoMapper(typeof(UserMappingProfile).Assembly); - - services.Configure(dispatcherConfigSection); - services.Configure(mailConfigSection); - - return services; - } - - public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config) => services.AddEnvelopeGenerator( - dispatcherConfigSection: config.GetSection("DispatcherConfig"), - mailConfigSection: config.GetSection("MailConfig")); - } -} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/ConfigDto.cs b/EnvelopeGenerator.Application/DTOs/ConfigDto.cs index f96f131d..2dc76824 100644 --- a/EnvelopeGenerator.Application/DTOs/ConfigDto.cs +++ b/EnvelopeGenerator.Application/DTOs/ConfigDto.cs @@ -1,4 +1,5 @@ using DigitalData.Core.Abstractions; +using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; namespace EnvelopeGenerator.Application.DTOs @@ -8,11 +9,9 @@ namespace EnvelopeGenerator.Application.DTOs int SendingProfile, string SignatureHost, string ExternalProgramName, - string ExportPath, - string DocumentPathDmz, - string ExportPathDmz, - string DocumentPathMoveAftsend) : IUnique + string ExportPath) : IUnique { + [NotMapped] [JsonIgnore] [Obsolete("Configuration does not have an ID; it represents a single table in the database.")] public int Id => throw new InvalidOperationException("This configuration does not support an ID as it represents a single row in the database."); diff --git a/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverBasicDto.cs b/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverBasicDto.cs index 4dfdf6a8..1302f14f 100644 --- a/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverBasicDto.cs +++ b/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverBasicDto.cs @@ -25,5 +25,9 @@ namespace EnvelopeGenerator.Application.DTOs.EnvelopeReceiver public DateTime AddedWhen { get; init; } public DateTime? ChangedWhen { get; init; } + + public bool HasPhoneNumber { get; init; } + + public bool TFAEnabled { get; init; } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs b/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs index 511c6d42..9470766e 100644 --- a/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs +++ b/EnvelopeGenerator.Application/DTOs/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs @@ -1,4 +1,9 @@ namespace EnvelopeGenerator.Application.DTOs.EnvelopeReceiver { - public record EnvelopeReceiverSecretDto(string? AccessCode) : EnvelopeReceiverDto; + public record EnvelopeReceiverSecretDto() : EnvelopeReceiverDto() + { + public string? AccessCode { get; init; } + + public string? PhoneNumber { get; init; } + } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/Messaging/SmsResponse.cs b/EnvelopeGenerator.Application/DTOs/Messaging/SmsResponse.cs new file mode 100644 index 00000000..96b67515 --- /dev/null +++ b/EnvelopeGenerator.Application/DTOs/Messaging/SmsResponse.cs @@ -0,0 +1,19 @@ +namespace EnvelopeGenerator.Application.DTOs.Messaging +{ + public record SmsResponse + { + 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; } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs index 949ee06a..87e4d3f9 100644 --- a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs +++ b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverCreateDto.cs @@ -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; diff --git a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs index ec6e5b76..6d1b5a19 100644 --- a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs +++ b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs @@ -1,4 +1,6 @@ -using DigitalData.Core.DTO; +using DigitalData.Core.Abstractions; +using DigitalData.Core.DTO; +using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; using System.Text.Json.Serialization; @@ -8,12 +10,17 @@ namespace EnvelopeGenerator.Application.DTOs.Receiver int Id, string EmailAddress, string Signature, - DateTime AddedWhen - ) : BaseDTO(Id) + DateTime AddedWhen + ) : BaseDTO(Id), IUnique { [JsonIgnore] public IEnumerable? EnvelopeReceivers { get; init; } public string? LastUsedName => EnvelopeReceivers?.LastOrDefault()?.Name; + + public string? TotpSecretkey { get; set; } = null; + + [TemplatePlaceholder("[TFA_QR_EXPIRATION]")] + public DateTime? TotpExpiration { get; set; } = null; }; } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs index 8f765667..08ec5616 100644 --- a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs +++ b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverUpdateDto.cs @@ -2,5 +2,5 @@ namespace EnvelopeGenerator.Application.DTOs.Receiver { - public record ReceiverUpdateDto(int Id) : IUnique; -} \ No newline at end of file + public record ReceiverUpdateDto(int Id, string? TotpSecretkey = null, DateTime? TotpExpiration = null) : IUnique; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj index f1267868..6032bb17 100644 --- a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj +++ b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj @@ -12,11 +12,14 @@ - + + + + diff --git a/EnvelopeGenerator.Application/Extensions/CacheExtensions.cs b/EnvelopeGenerator.Application/Extensions/CacheExtensions.cs new file mode 100644 index 00000000..45f43109 --- /dev/null +++ b/EnvelopeGenerator.Application/Extensions/CacheExtensions.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Caching.Distributed; + +namespace EnvelopeGenerator.Application.Extensions +{ + public static class CacheExtensions + { + public static Task SetLongAsync(this IDistributedCache cache, string key, long value, DistributedCacheEntryOptions? options = null) + => options is null + ? cache.SetAsync(key, BitConverter.GetBytes(value)) + : cache.SetAsync(key, BitConverter.GetBytes(value), options: options); + + public static async Task GetLongAsync(this IDistributedCache cache, string key) + { + var value = await cache.GetAsync(key); + 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 async Task GetDateTimeAsync(this IDistributedCache cache, string key) + { + var value = await cache.GetAsync(key); + 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 async Task GetTimeSpanAsync(this IDistributedCache cache, string key) + { + var value = await cache.GetAsync(key); + return value is null ? null : new(BitConverter.ToInt64(value, 0)); + } + + public static string GetOrSet(this IDistributedCache cache, string key, Func factory, DistributedCacheEntryOptions? options = null, bool cacheInBackground = false, CancellationToken token = default) + { + var value = cache.GetString(key); + 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 GetOrSetAsync(this IDistributedCache cache, string key, Func> 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); + + if (cacheInBackground) + _ = Task.Run(async () => await CacheAsync(), token); + else + await CacheAsync(); + } + + return value; + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Extensions/DIExtensions.cs b/EnvelopeGenerator.Application/Extensions/DIExtensions.cs new file mode 100644 index 00000000..ae8fedf9 --- /dev/null +++ b/EnvelopeGenerator.Application/Extensions/DIExtensions.cs @@ -0,0 +1,78 @@ +using DigitalData.UserManager.Application.MappingProfiles; +using EnvelopeGenerator.Application.Contracts; +using EnvelopeGenerator.Application.MappingProfiles; +using EnvelopeGenerator.Application.Configurations; +using EnvelopeGenerator.Application.Services; +using EnvelopeGenerator.Infrastructure.Contracts; +using EnvelopeGenerator.Infrastructure.Repositories; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using DigitalData.Core.Client; +using EnvelopeGenerator.Application.Configurations.GtxMessaging; +using QRCoder; + +namespace EnvelopeGenerator.Application.Extensions +{ + public static class DIExtensions + { + public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfigurationSection dispatcherConfigSection, IConfigurationSection mailConfigSection, IConfigurationSection smsConfigSection, IConfigurationSection codeGeneratorConfigSection, IConfigurationSection envelopeReceiverCacheParamsSection) + { + //Inject CRUD Service and repositoriesad + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + //Auto mapping profiles + services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly); + services.AddAutoMapper(typeof(UserMappingProfile).Assembly); + + services.Configure(dispatcherConfigSection); + services.Configure(mailConfigSection); + services.Configure(codeGeneratorConfigSection); + services.Configure(envelopeReceiverCacheParamsSection); + + services.AddHttpClientService(smsConfigSection); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } + + public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config) => services.AddEnvelopeGenerator( + dispatcherConfigSection: config.GetSection("DispatcherConfig"), + mailConfigSection: config.GetSection("MailConfig"), + smsConfigSection: config.GetSection("SmsConfig"), + codeGeneratorConfigSection: config.GetSection("CodeGeneratorParams"), + envelopeReceiverCacheParamsSection: config.GetSection("EnvelopeReceiverCacheParams")); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Extensions/DTOExtensions.cs b/EnvelopeGenerator.Application/Extensions/DTOExtensions.cs new file mode 100644 index 00000000..f1cd28ca --- /dev/null +++ b/EnvelopeGenerator.Application/Extensions/DTOExtensions.cs @@ -0,0 +1,22 @@ +using EnvelopeGenerator.Application.DTOs.Receiver; +using EnvelopeGenerator.Extensions; +using Newtonsoft.Json; + +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); + + public static bool IsTotpValid(this ReceiverReadDto dto, string totp) => dto.TotpSecretkey is null ? throw new ArgumentNullException(nameof(dto), $"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(dto)}") : totp.IsValidTotp(dto.TotpSecretkey); + + public static bool IsTotpInvalid(this ReceiverReadDto dto, string totp) => !dto.IsTotpValid(totp: totp); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Extensions/MappingExtensions.cs b/EnvelopeGenerator.Application/Extensions/MappingExtensions.cs new file mode 100644 index 00000000..bd996b58 --- /dev/null +++ b/EnvelopeGenerator.Application/Extensions/MappingExtensions.cs @@ -0,0 +1,14 @@ +using EnvelopeGenerator.Domain.HttpResponse; + +namespace EnvelopeGenerator.Application.Extensions +{ + public static class MappingExtensions + { + 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); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Key.cs b/EnvelopeGenerator.Application/Key.cs index 4509d0ea..52f35d19 100644 --- a/EnvelopeGenerator.Application/Key.cs +++ b/EnvelopeGenerator.Application/Key.cs @@ -14,6 +14,7 @@ public static readonly string PossibleSecurityBreach = nameof(PossibleSecurityBreach); public static readonly string WrongEnvelopeReceiverId = nameof(WrongEnvelopeReceiverId); public static readonly string EnvelopeOrReceiverNonexists = nameof(EnvelopeOrReceiverNonexists); + public static readonly string PhoneNumberNonexists = nameof(PhoneNumberNonexists); public static readonly string Default = nameof(Default); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/MappingProfiles/BasicDtoMappingProfile.cs b/EnvelopeGenerator.Application/MappingProfiles/BasicDtoMappingProfile.cs index 18462893..cf3c006b 100644 --- a/EnvelopeGenerator.Application/MappingProfiles/BasicDtoMappingProfile.cs +++ b/EnvelopeGenerator.Application/MappingProfiles/BasicDtoMappingProfile.cs @@ -3,8 +3,11 @@ using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs.EnvelopeHistory; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiverReadOnly; +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 { @@ -43,13 +46,20 @@ namespace EnvelopeGenerator.Application.MappingProfiles CreateMap(); CreateMap(); CreateMap(); - CreateMap(); + CreateMap().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore()); CreateMap(); CreateMap(); CreateMap(); CreateMap(); CreateMap(); CreateMap(); + + // Messaging mappings + // for GTX messaging + CreateMap() + .ConstructUsing(gtxRes => gtxRes.Ok() + ? new SmsResponse() { Ok = true } + : new SmsResponse() { Ok = false, Errors = gtxRes }); } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx index 244fbc28..6a951a19 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx @@ -159,27 +159,60 @@ Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente. - - Öffnen - Bitte überprüfen Sie die Standortinformationen. Wenn sie falsch sind, korrigieren Sie diese bitte. - - Zugriffscode - - + Wir haben Ihnen gerade den Zugriffscode an die hinterlegte Email Adresse gesendet. Dies kann evtl. einige Minuten dauern. - + + Ihr QR-Code ist bis {0} gültig. + + + Wir haben den QR-Code an Ihre E-Mail-Adresse gesendet. Ihr QR-Code ist bis {0} gültig. Sie können ihn für alle Umschläge verwenden, die Sie an diese E-Mail-Adresse erhalten. + + + Wir haben soeben den Zugangscode als SMS an die von Ihnen angegebene Telefonnummer gesendet. + + + Zugriffscode + + + TOTP + + + SMS-Code + + Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender bitten, Ihnen den Code auf anderem Wege zukommen zu lassen. - + + Der neue QR-Code wird nur einmal für einen bestimmten Zeitraum gesendet und nach dem Scannen in Ihrer Authenticator-App gespeichert. Er kann für alle Umschläge verwendet werden, die an dieselbe E-Mail-Adresse gesendet werden, bis er abläuft. Wenn Sie die QR-Code-Mail nicht erhalten oder sie sowohl aus der Mail als auch aus authenticator löschen, kontaktieren Sie bitte den Absender. + + + Sie können den Absender bitten, Ihre Rufnummer zu überprüfen. Die Telefonnummer muss mit der Ortsvorwahl eingegeben werden. Andernfalls können Sie beantragen, den Zwei-Faktor-Schutz zu entfernen. + + Sie haben keinen Zugriffscode erhalten? - + + Sie haben keinen QR-Code erhalten? + + + Sie haben keine SMS erhalten? + + Dokument erfordert einen Zugriffscode + + 2-Faktor-Authentifizierung + + + 2-Faktor-Authentifizierung + + + Datenschutz + Weitergeleitet von {0}. Gültig bis {1}. diff --git a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx index b99951ec..4ae7197e 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx @@ -159,27 +159,60 @@ The Digital Signature Portal is a platform developed for securely signing and managing your documents. With its user-friendly interface, you can quickly upload your documents, track the signing processes, and easily carry out your digital signature applications. This portal accelerates your workflow with legally valid signatures while enhancing the security of your documents. - - Open - Please review the location information. If it is incorrect, kindly make the necessary corrections. - - Access Code - - + We have just sent you the access code to the email address you provided. This may take a few minutes. - + + Your QR code is valid until {0}. + + + We have sent the QR code to your e-mail address. Your QR code is valid until {0}. You can use it for all envelopes received at this email address. + + + We have just sent the access code as an SMS to the phone number you provided. + + + Access Code + + + TOTP + + + SMS Code + + Please check your email inbox including your spam folder. Furthermore, you can also ask the sender to send the code by other means. - + + The new QR code is sent only once for a given period and is saved in your authenticator app once scanned. It can be used for all envelopes received at the same email address until it expires. If you do not receive the QR code mail or delete it both from the mail and from authenticator, please contact the sender. + + + You can ask the sender to check your phone number. The phone number must be entered with the area code. Otherwise you can request to remove the two-factor protection. + + You have not received an access code? - + + You have not received a QR code? + + + You have not received an SMS? + + Document requires an access code + + 2-Factor Authentication + + + 2-Factor Authentication + + + Privacy + Forwarded by {0}. Valid until {1}. diff --git a/EnvelopeGenerator.Application/Services/CodeGenerator.cs b/EnvelopeGenerator.Application/Services/CodeGenerator.cs new file mode 100644 index 00000000..89d22522 --- /dev/null +++ b/EnvelopeGenerator.Application/Services/CodeGenerator.cs @@ -0,0 +1,66 @@ +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 LazyStatic => new(() => new CodeGenerator(Options.Create(new()), new QRCodeGenerator())); + + public static CodeGenerator Static => LazyStatic.Value; + + private readonly CodeGeneratorParams _params; + + private readonly QRCodeGenerator _qrCodeGenerator; + + public CodeGenerator(IOptions options, QRCodeGenerator qrCodeGenerator) + { + _params = options.Value; + _qrCodeGenerator = qrCodeGenerator; + } + + public string GenerateCode(int length) + { + //TODO: Inject Random as a singleton to support multithreading to improve performance. + Random random = new(); + + if (length <= 0) + throw new ArgumentException("Password length must be greater than 0."); + + var passwordBuilder = new StringBuilder(length); + + for (int i = 0; i < length; i++) + 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); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs b/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs index 5e6ca13e..99a77ca0 100644 --- a/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs +++ b/EnvelopeGenerator.Application/Services/EnvelopeMailService.cs @@ -12,6 +12,8 @@ using static EnvelopeGenerator.Common.Constants; using EnvelopeGenerator.Extensions; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiverReadOnly; using EnvelopeGenerator.Application.Configurations; +using EnvelopeGenerator.Application.Extensions; +using Newtonsoft.Json; namespace EnvelopeGenerator.Application.Services { @@ -22,17 +24,19 @@ namespace EnvelopeGenerator.Application.Services private readonly DispatcherConfig _dConfig; private readonly IConfigService _configService; private readonly Dictionary _placeholders; + private readonly ICodeGenerator _codeGenerator; - public EnvelopeMailService(IEmailOutRepository repository, IMapper mapper, IEmailTemplateService tempService, IEnvelopeReceiverService envelopeReceiverService, IOptions dispatcherConfigOptions, IConfigService configService, IOptions mailConfig) : base(repository, mapper) + public EnvelopeMailService(IEmailOutRepository repository, IMapper mapper, IEmailTemplateService tempService, IEnvelopeReceiverService envelopeReceiverService, IOptions dispatcherConfigOptions, IConfigService configService, IOptions mailConfig, ICodeGenerator codeGenerator) : base(repository, mapper) { _tempService = tempService; _envRcvService = envelopeReceiverService; _dConfig = dispatcherConfigOptions.Value; _configService = configService; _placeholders = mailConfig.Value.Placeholders; + _codeGenerator = codeGenerator; } - private async Task> CreatePlaceholders(string? accessCode = null, EnvelopeReceiverDto? envelopeReceiverDto = null, EnvelopeReceiverReadOnlyDto? readOnlyDto = null) + private async Task> CreatePlaceholders(string? accessCode = null, EnvelopeReceiverDto? envelopeReceiverDto = null) { if (accessCode is not null) _placeholders["[DOCUMENT_ACCESS_CODE]"] = accessCode; @@ -63,10 +67,8 @@ namespace EnvelopeGenerator.Application.Services return _placeholders; } - - public async Task> SendAccessCodeAsync(EnvelopeReceiverDto dto) => await SendAsync(dto: dto, tempType: Constants.EmailTemplateType.DocumentAccessCodeReceived); - - public async Task> SendAsync(EnvelopeReceiverDto dto, Constants.EmailTemplateType tempType) + + public async Task> SendAsync(EnvelopeReceiverDto dto, EmailTemplateType tempType, Dictionary? optionalPlaceholders = null) { var tempSerResult = await _tempService.ReadByNameAsync(tempType); if (tempSerResult.IsFailed) @@ -104,14 +106,19 @@ namespace EnvelopeGenerator.Application.Services var placeholders = await CreatePlaceholders(accessCode: accessCode, envelopeReceiverDto: dto); + // Add optional place holders. + if (optionalPlaceholders is not null) + foreach (var oph in optionalPlaceholders) + placeholders[oph.Key] = oph.Value.ToString() ?? "NULL"; + //TODO: remove the requirement to add the models using reflections return await CreateWithTemplateAsync(createDto: mail,placeholders: placeholders, dto, dto.Envelope.User!, dto.Envelope); } - public async Task> SendAsync(EnvelopeReceiverReadOnlyDto dto) + public async Task> SendAsync(EnvelopeReceiverReadOnlyDto dto, Dictionary? optionalPlaceholders = null) { - var tempSerResult = await _tempService.ReadByNameAsync(Constants.EmailTemplateType.DocumentShared); + var tempSerResult = await _tempService.ReadByNameAsync(EmailTemplateType.DocumentShared); if (tempSerResult.IsFailed) return tempSerResult.ToFail().Notice(LogLevel.Error, Flag.DataIntegrityIssue, $"The email cannot send because '{Constants.EmailTemplateType.DocumentShared}' template cannot found."); var temp = tempSerResult.Data; @@ -140,7 +147,32 @@ namespace EnvelopeGenerator.Application.Services var placeholders = await CreatePlaceholders(readOnlyDto: dto); + // Add optional place holders. + if (optionalPlaceholders is not null) + foreach (var oph in optionalPlaceholders) + placeholders[oph.Key] = oph.Value.ToString() ?? "NULL"; + return await CreateWithTemplateAsync(createDto: mail, placeholders: placeholders, dto.Envelope); } + + public async Task> SendAccessCodeAsync(EnvelopeReceiverDto dto) => await SendAsync(dto: dto, tempType: EmailTemplateType.DocumentAccessCodeReceived); + + public Task> SendTFAQrCodeAsync(EnvelopeReceiverDto dto) + { + // Check if receiver or secret key is null + if (dto.Receiver is null) + throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver information is missing. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}"); + if (dto.Receiver.TotpSecretkey is null) + throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver.TotpSecretKey is null. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}"); + if (dto.Receiver.TotpExpiration is null) + throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver.TotpExpiration is null. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}"); + + var totp_qr_64 = _codeGenerator.GenerateTotpQrCode(userEmail: dto.Receiver.EmailAddress, secretKey: dto.Receiver.TotpSecretkey).ToBase64String(); + return SendAsync(dto, EmailTemplateType.TotpSecret, new() + { + {"[TFA_QR_CODE]", totp_qr_64 }, + {"[TFA_EXPIRATION]", dto.Receiver.TotpExpiration } + }); + } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Services/EnvelopeReceiverCache.cs b/EnvelopeGenerator.Application/Services/EnvelopeReceiverCache.cs new file mode 100644 index 00000000..525b5468 --- /dev/null +++ b/EnvelopeGenerator.Application/Services/EnvelopeReceiverCache.cs @@ -0,0 +1,50 @@ +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 cacheParamOptions, IDistributedCache cache) + { + _cacheParams = cacheParamOptions.Value; + _codeCacheOptions = new() { AbsoluteExpirationRelativeToNow = cacheParamOptions.Value.CodeCacheValidityPeriod }; + _cache = cache; + } + + public async Task GetSmsCodeAsync(string envelopeReceiverId) + { + var code_key = string.Format(_cacheParams.CodeCacheKeyFormat, envelopeReceiverId); + return await _cache.GetStringAsync(code_key); + } + + public async Task 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 GetSmsCodeExpirationAsync(string envelopeReceiverId) + { + var code_expiration_key = string.Format(_cacheParams.CodeExpirationCacheKeyFormat, envelopeReceiverId); + return await _cache.GetDateTimeAsync(code_expiration_key); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs b/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs index 12b5ea11..ee3e07fe 100644 --- a/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs +++ b/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs @@ -9,6 +9,7 @@ using EnvelopeGenerator.Infrastructure.Contracts; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using EnvelopeGenerator.Extensions; +using EnvelopeGenerator.Application.DTOs.Messaging; namespace EnvelopeGenerator.Application.Services { @@ -16,33 +17,36 @@ namespace EnvelopeGenerator.Application.Services { private readonly IStringLocalizer _localizer; - public EnvelopeReceiverService(IEnvelopeReceiverRepository repository, IStringLocalizer localizer, IMapper mapper) + private readonly IMessagingService _messagingService; + + public EnvelopeReceiverService(IEnvelopeReceiverRepository repository, IStringLocalizer localizer, IMapper mapper, IMessagingService messagingService) : base(repository, mapper) { _localizer = localizer; + _messagingService = messagingService; } - public async Task>> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true) + public async Task>> 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>(env_rcvs)); } - public async Task>> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false) + public async Task>> 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>(env_rcvs)); } - public async Task>> ReadSecretByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true) + public async Task>> ReadAccessCodeByUuidAsync(string uuid, bool withEnvelope = false, bool withReceiver = true) { var env_rcvs = await _repository.ReadByUuidAsync(uuid: uuid, withEnvelope: withEnvelope, withReceiver: withReceiver); - return Result.Success(_mapper.Map>(env_rcvs)); + return Result.Success(env_rcvs.Select(er => er.AccessCode)); } - public async Task> ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true) + public async Task> 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() .Message(Key.EnvelopeReceiverNotFound); @@ -50,7 +54,17 @@ namespace EnvelopeGenerator.Application.Services return Result.Success(_mapper.Map(env_rcv)); } - public async Task> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true) + public async Task> 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, readOnly: readOnly); + if (env_rcv is null) + return Result.Fail() + .Message(Key.EnvelopeReceiverNotFound); + + return Result.Success(_mapper.Map(env_rcv)); + } + + public async Task> ReadByEnvelopeReceiverIdAsync(string envelopeReceiverId, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true) { (string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId(); @@ -61,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> VerifyAccessCodeAsync(string uuid, string signature, string accessCode) @@ -135,5 +149,31 @@ namespace EnvelopeGenerator.Application.Services var er = await _repository.ReadLastByReceiver(mail); return er is null ? Result.Fail().Notice(LogLevel.None, Flag.NotFound) : Result.Success(er.Name); } + + public async Task> SendSmsAsync(string envelopeReceiverId, string message) + { + (string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId(); + + if (uuid is null || signature is null) + return Result.Fail() + .Message(_localizer[Key.WrongEnvelopeReceiverId]) + .Notice(LogLevel.Warning, (uuid, signature).ToTitle()) + .Notice(LogLevel.Warning, EnvelopeFlag.WrongEnvelopeReceiverId) + .Notice(LogLevel.Warning, Flag.PossibleSecurityBreach); + + var env_rcv = await _repository.ReadByUuidSignatureAsync(uuid: uuid, signature: signature, withEnvelope: false, withReceiver: false); + if (env_rcv is null) + return Result.Fail() + .Message(Key.EnvelopeReceiverNotFound); + + if (env_rcv.PhoneNumber is null) + return Result.Fail() + .Message(Key.PhoneNumberNonexists) + .Notice(LogLevel.Error, Flag.NotFound, $"An attempt was made to send sms to the user whose phone number is null. Envelope recipient ID is {envelopeReceiverId}, UUID is {uuid} and signature is {signature}."); + + var res = await _messagingService.SendSmsAsync(recipient: env_rcv.PhoneNumber, message: message); + + return Result.Success(res); + } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Services/GTXMessagingService.cs b/EnvelopeGenerator.Application/Services/GTXMessagingService.cs new file mode 100644 index 00000000..17d179eb --- /dev/null +++ b/EnvelopeGenerator.Application/Services/GTXMessagingService.cs @@ -0,0 +1,70 @@ +using AutoMapper; +using DigitalData.Core.Abstractions.Client; +using DigitalData.Core.Client; +using EnvelopeGenerator.Application.Configurations.GtxMessaging; +using EnvelopeGenerator.Application.Contracts; +using EnvelopeGenerator.Application.DTOs.Messaging; +using EnvelopeGenerator.Application.Extensions; +using EnvelopeGenerator.Domain.HttpResponse; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Application.Services +{ + public class GtxMessagingService : IMessagingService + { + private readonly IHttpClientService _smsClient; + + private readonly SmsParams _smsParams; + + private readonly IMapper _mapper; + + private readonly ICodeGenerator _codeGen; + + private readonly IEnvelopeReceiverCache _erCache; + + public string ServiceProvider { get; } + + public GtxMessagingService(IHttpClientService smsClient, IOptions smsParamsOptions, IMapper mapper, ICodeGenerator codeGenerator, IEnvelopeReceiverCache envelopeReceiverCache) + { + _smsClient = smsClient; + _smsParams = smsParamsOptions.Value; + _mapper = mapper; + ServiceProvider = GetType().Name.Replace("Service", string.Empty); + _codeGen = codeGenerator; + _erCache = envelopeReceiverCache; + } + + public async Task SendSmsAsync(string recipient, string message) + { + return await _smsClient.FetchAsync(queryParams: new Dictionary() + { + { _smsParams.RecipientQueryParamName, recipient }, + { _smsParams.MessageQueryParamName, message } + }) + .ThenAsync(res => res.Json()) + .ThenAsync(_mapper.Map); + } + + public async Task 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 }; + } + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Services/ReceiverService.cs b/EnvelopeGenerator.Application/Services/ReceiverService.cs index 422f5c0f..97015953 100644 --- a/EnvelopeGenerator.Application/Services/ReceiverService.cs +++ b/EnvelopeGenerator.Application/Services/ReceiverService.cs @@ -5,6 +5,8 @@ using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Application.DTOs.Receiver; using DigitalData.Core.DTO; +using DigitalData.Core.Abstractions; +using Microsoft.Extensions.Logging; namespace EnvelopeGenerator.Application.Services { @@ -34,5 +36,17 @@ namespace EnvelopeGenerator.Application.Services return await _repository.DeleteAsync(rcv) ? Result.Success() : Result.Fail(); } + + public virtual async Task UpdateAsync(TUpdateDto updateDto) where TUpdateDto : IUnique + { + var val = await _repository.ReadByIdAsync(updateDto.Id); + if (val == null) + { + return Result.Fail().Notice(LogLevel.Warning, Flag.NotFound, $"{updateDto.Id} is not found in update process of {GetType()} entity."); + } + + var entity = _mapper.Map(updateDto, val); + return (await _repository.UpdateAsync(entity)) ? Result.Success() : Result.Fail(); + } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Common/Constants.vb b/EnvelopeGenerator.Common/Constants.vb index 969aac71..2fd6c739 100644 --- a/EnvelopeGenerator.Common/Constants.vb +++ b/EnvelopeGenerator.Common/Constants.vb @@ -100,6 +100,7 @@ DocumentCompleted DocumentAccessCodeReceived DocumentShared + TotpSecret End Enum Public Enum EncodeType diff --git a/EnvelopeGenerator.Domain/Entities/Config.cs b/EnvelopeGenerator.Domain/Entities/Config.cs index 6621beb5..0f5d48e5 100644 --- a/EnvelopeGenerator.Domain/Entities/Config.cs +++ b/EnvelopeGenerator.Domain/Entities/Config.cs @@ -1,6 +1,7 @@ using DigitalData.Core.Abstractions; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; namespace EnvelopeGenerator.Domain.Entities { @@ -23,19 +24,9 @@ namespace EnvelopeGenerator.Domain.Entities [Column("EXPORT_PATH", TypeName = "nvarchar(256)")] public string? ExportPath { get; init; } - [Column("DOCUMENT_PATH_DMZ", TypeName = "nvarchar(512)")] - [Required] - public string? DocumentPathDmz { get; init; } - - [Column("EXPORT_PATH_DMZ", TypeName = "nvarchar(512)")] - [Required] - public required string ExportPathDmz { get; init; } - - [Column("DOCUMENT_PATH_MOVE_AFTSEND", TypeName = "nvarchar(512)")] - [Required] - public required string DocumentPathMoveAftsend { get; init; } - [Obsolete("Configuration does not have an ID; it represents a single table in the database.")] + [NotMapped] + [JsonIgnore] public int Id => throw new InvalidOperationException("This configuration does not support an ID as it represents a single table in the database."); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Domain/Entities/EnvelopeDocument.cs b/EnvelopeGenerator.Domain/Entities/EnvelopeDocument.cs index 85d9ce77..97723f0a 100644 --- a/EnvelopeGenerator.Domain/Entities/EnvelopeDocument.cs +++ b/EnvelopeGenerator.Domain/Entities/EnvelopeDocument.cs @@ -16,21 +16,10 @@ namespace EnvelopeGenerator.Domain.Entities [Column("ENVELOPE_ID")] public int EnvelopeId { get; set; } - [Required] - [Column("FILENAME", TypeName = "nvarchar(256)")] - public required string Filename { get; set; } - - [Required] - [Column("FILEPATH", TypeName = "nvarchar(256)")] - public required string Filepath { get; set; } - [Required] [Column("ADDED_WHEN", TypeName = "datetime")] public required DateTime AddedWhen { get; set; } - [Column("FILENAME_ORIGINAL", TypeName = "nvarchar(256)")] - public required string FilenameOriginal { get; set; } - [Column("BYTE_DATA", TypeName = "varbinary(max)")] public byte[]? ByteData { get; init; } diff --git a/EnvelopeGenerator.Domain/Entities/EnvelopeReceiverBase.cs b/EnvelopeGenerator.Domain/Entities/EnvelopeReceiverBase.cs index ef934a19..64bc6802 100644 --- a/EnvelopeGenerator.Domain/Entities/EnvelopeReceiverBase.cs +++ b/EnvelopeGenerator.Domain/Entities/EnvelopeReceiverBase.cs @@ -41,6 +41,18 @@ namespace EnvelopeGenerator.Domain.Entities [Column("CHANGED_WHEN", TypeName = "datetime")] public DateTime? ChangedWhen { get; set; } + [Column("PHONE_NUMBER")] + [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); + + [NotMapped] + public bool HasPhoneNumber => PhoneNumber is not null; } } \ No newline at end of file diff --git a/EnvelopeGenerator.Domain/Entities/Receiver.cs b/EnvelopeGenerator.Domain/Entities/Receiver.cs index ae39daf4..da0928f8 100644 --- a/EnvelopeGenerator.Domain/Entities/Receiver.cs +++ b/EnvelopeGenerator.Domain/Entities/Receiver.cs @@ -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? EnvelopeReceivers { get; init; } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj b/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj index 3480802f..50561806 100644 --- a/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj +++ b/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj @@ -7,7 +7,7 @@ - + diff --git a/EnvelopeGenerator.Domain/HttpResponse/GtxMessagingResponse.cs b/EnvelopeGenerator.Domain/HttpResponse/GtxMessagingResponse.cs new file mode 100644 index 00000000..cc948e52 --- /dev/null +++ b/EnvelopeGenerator.Domain/HttpResponse/GtxMessagingResponse.cs @@ -0,0 +1,4 @@ +namespace EnvelopeGenerator.Domain.HttpResponse +{ + public class GtxMessagingResponse : Dictionary { } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj b/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj index 8467bc11..f4ea6b21 100644 --- a/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj +++ b/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj @@ -10,6 +10,7 @@ + diff --git a/EnvelopeGenerator.Extensions/StringExtension.cs b/EnvelopeGenerator.Extensions/StringExtension.cs new file mode 100644 index 00000000..b08a0f66 --- /dev/null +++ b/EnvelopeGenerator.Extensions/StringExtension.cs @@ -0,0 +1,14 @@ +using OtpNet; + +namespace EnvelopeGenerator.Extensions +{ + public static class StringExtension + { + public static bool IsValidTotp(this string totp, string secret) + { + var secret_bytes = Base32Encoding.ToBytes(secret); + var secret_totp = new Totp(secret_bytes); + return secret_totp.VerifyTotp(totp, out _, VerificationWindow.RfcSpecifiedNetworkDelay); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj b/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj index 2684ed2c..51efca53 100644 --- a/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj +++ b/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj @@ -7,7 +7,7 @@ - + diff --git a/EnvelopeGenerator.GeneratorAPI/Program.cs b/EnvelopeGenerator.GeneratorAPI/Program.cs index d95f039b..5d734ea5 100644 --- a/EnvelopeGenerator.GeneratorAPI/Program.cs +++ b/EnvelopeGenerator.GeneratorAPI/Program.cs @@ -1,7 +1,7 @@ using DigitalData.Core.API; using DigitalData.Core.Application; using DigitalData.UserManager.Application; -using EnvelopeGenerator.Application; +using EnvelopeGenerator.Application.Extensions; using EnvelopeGenerator.Infrastructure; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Localization; diff --git a/EnvelopeGenerator.Infrastructure/Contracts/IEnvelopeReceiverRepository.cs b/EnvelopeGenerator.Infrastructure/Contracts/IEnvelopeReceiverRepository.cs index e471ddf7..ffd56528 100644 --- a/EnvelopeGenerator.Infrastructure/Contracts/IEnvelopeReceiverRepository.cs +++ b/EnvelopeGenerator.Infrastructure/Contracts/IEnvelopeReceiverRepository.cs @@ -5,19 +5,19 @@ namespace EnvelopeGenerator.Infrastructure.Contracts { public interface IEnvelopeReceiverRepository : ICRUDRepository { - Task> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false); + Task> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false, bool readOnly = true); - Task> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true); + Task> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true, bool readOnly = true); - Task ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true); + Task ReadByUuidSignatureAsync(string uuid, string signature, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true); - Task ReadAccessCodeAsync(string uuid, string signature); + Task ReadAccessCodeAsync(string uuid, string signature, bool readOnly = true); Task CountAsync(string uuid, string signature); - Task ReadByIdAsync(int envelopeId, int receiverId); + Task ReadByIdAsync(int envelopeId, int receiverId, bool readOnly = true); - Task ReadAccessCodeByIdAsync(int envelopeId, int receiverId); + Task ReadAccessCodeByIdAsync(int envelopeId, int receiverId, bool readOnly = true); Task> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses); diff --git a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj index f295ff69..146af618 100644 --- a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj +++ b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj @@ -7,7 +7,7 @@ - + diff --git a/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs b/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs index 7b72f29e..9bfb26e4 100644 --- a/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs +++ b/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs @@ -11,9 +11,9 @@ namespace EnvelopeGenerator.Infrastructure.Repositories { } - private IQueryable ReadWhere(string? uuid = null, string? signature = null, bool withEnvelope = false, bool withReceiver = false) + private IQueryable 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> ReadByUuidAsync(string uuid, bool withEnvelope = true, bool withReceiver = false) - => await ReadWhere(uuid: uuid, withEnvelope: withEnvelope, withReceiver: withReceiver).ToListAsync(); + public async Task> 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> ReadBySignatureAsync(string signature, bool withEnvelope = false, bool withReceiver = true) - => await ReadWhere(signature: signature, withEnvelope: withEnvelope, withReceiver: withReceiver).ToListAsync(); + public async Task> 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 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 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 ReadAccessCodeAsync(string uuid, string signature) - => await ReadWhere(uuid: uuid, signature: signature) + public async Task ReadAccessCodeAsync(string uuid, string signature, bool readOnly = true) + => await ReadWhere(uuid: uuid, signature: signature, readOnly: readOnly) .Select(er => er.AccessCode) .FirstOrDefaultAsync(); public async Task CountAsync(string uuid, string signature) => await ReadWhere(uuid: uuid, signature: signature).CountAsync(); - public IQueryable ReadById(int envelopeId, int receiverId) => _dbSet.AsNoTracking() - .Where(er => er.EnvelopeId == envelopeId && er.ReceiverId == receiverId); + private IQueryable 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 ReadByIdAsync(int envelopeId, int receiverId) - => await ReadById(envelopeId: envelopeId, receiverId: receiverId) + public async Task ReadByIdAsync(int envelopeId, int receiverId, bool readOnly = true) + => await ReadById(envelopeId: envelopeId, receiverId: receiverId, readOnly: readOnly) .FirstOrDefaultAsync(); - public async Task ReadAccessCodeByIdAsync(int envelopeId, int receiverId) - => await ReadById(envelopeId: envelopeId, receiverId: receiverId) + public async Task ReadAccessCodeByIdAsync(int envelopeId, int receiverId, bool readOnly = true) + => await ReadById(envelopeId: envelopeId, receiverId: receiverId, readOnly: readOnly) .Select(er => er.AccessCode) .FirstOrDefaultAsync(); diff --git a/EnvelopeGenerator.Web/Controllers/HomeController.cs b/EnvelopeGenerator.Web/Controllers/HomeController.cs index ad5687d7..d67d9b86 100644 --- a/EnvelopeGenerator.Web/Controllers/HomeController.cs +++ b/EnvelopeGenerator.Web/Controllers/HomeController.cs @@ -18,7 +18,8 @@ using static EnvelopeGenerator.Common.Constants; using Ganss.Xss; using Newtonsoft.Json; using EnvelopeGenerator.Application.DTOs; -using EnvelopeGenerator.Domain.Entities; +using DigitalData.Core.Client; +using EnvelopeGenerator.Application.Extensions; namespace EnvelopeGenerator.Web.Controllers { @@ -34,8 +35,12 @@ namespace EnvelopeGenerator.Web.Controllers private readonly Cultures _cultures; private readonly IEnvelopeMailService _mailService; private readonly IEnvelopeReceiverReadOnlyService _readOnlyService; + private readonly IMessagingService _msgService; + private readonly IEnvelopeReceiverCache _erCache; + private readonly ICodeGenerator _codeGenerator; + private readonly IReceiverService _rcvService; - public HomeController(EnvelopeOldService envelopeOldService, ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService) + public HomeController(EnvelopeOldService envelopeOldService, ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IMessagingService messagingService, IEnvelopeReceiverCache envelopeReceiverCache, ICodeGenerator codeGenerator, IReceiverService receiverService) { this.envelopeOldService = envelopeOldService; _envRcvService = envelopeReceiverService; @@ -47,6 +52,10 @@ namespace EnvelopeGenerator.Web.Controllers _mailService = envelopeMailService; _logger = logger; _readOnlyService = readOnlyService; + _msgService = messagingService; + _erCache = envelopeReceiverCache; + _codeGenerator = codeGenerator; + _rcvService = receiverService; } [HttpGet("/")] @@ -102,7 +111,7 @@ namespace EnvelopeGenerator.Web.Controllers bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress); if (!accessCodeAlreadyRequested) { - await _historyService.RecordAsync(er.EnvelopeId, er.Receiver.EmailAddress, Constants.EnvelopeStatus.AccessCodeRequested); + await _historyService.RecordAsync(er.EnvelopeId, er.Receiver.EmailAddress, EnvelopeStatus.AccessCodeRequested); var mailRes = await _mailService.SendAccessCodeAsync(envelopeReceiverDto: er); if (mailRes.IsFailed) @@ -134,9 +143,12 @@ namespace EnvelopeGenerator.Web.Controllers { ViewData["UserCulture"] = _cultures[UserLanguage]; - return await _envRcvService.IsExisting(envelopeReceiverId: envelopeReceiverId).ThenAsync( - Success: isExisting => isExisting ? View().WithData("EnvelopeKey", envelopeReceiverId) : this.ViewEnvelopeNotFound(), - Fail: IActionResult (messages,notices) => + return await _envRcvService.ReadByEnvelopeReceiverIdAsync(envelopeReceiverId: envelopeReceiverId).ThenAsync( + Success: er => View() + .WithData("EnvelopeKey", envelopeReceiverId) + .WithData("TFAEnabled", er.TFAEnabled) + .WithData("HasPhoneNumber", er.HasPhoneNumber), + Fail: IActionResult (messages, notices) => { _logger.LogNotice(notices); Response.StatusCode = StatusCodes.Status401Unauthorized; @@ -151,7 +163,7 @@ namespace EnvelopeGenerator.Web.Controllers } [HttpPost("EnvelopeKey/{envelopeReceiverId}/Locked")] - public async Task LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] string access_code) + public async Task LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth) { try { @@ -166,33 +178,102 @@ namespace EnvelopeGenerator.Web.Controllers return Unauthorized(); } - _logger.LogInformation($"Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]"); + _logger.LogInformation("Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]", uuid, signature); //check access code EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId); - var verification = await _envRcvService.VerifyAccessCodeAsync(uuid: uuid, signature: signature, accessCode: access_code); - if (verification.IsFailed) - { - _logger.LogNotice(verification.Notices); - Response.StatusCode = StatusCodes.Status401Unauthorized; - return View("EnvelopeLocked") - .WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value); - } - return await _envRcvService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync( - SuccessAsync: async er => + return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync( + SuccessAsync: async er_secret => { - //check the access code verification - if (verification.IsWrong()) - { - //Constants.EnvelopeStatus.AccessCodeIncorrect - await _historyService.RecordAsync(er.EnvelopeId, er.Receiver!.EmailAddress, Constants.EnvelopeStatus.AccessCodeIncorrect); - Response.StatusCode = StatusCodes.Status401Unauthorized; - return View("EnvelopeLocked") - .WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value); - } + async Task 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); + } + } - await _historyService.RecordAsync(er.EnvelopeId, er.Receiver!.EmailAddress, Constants.EnvelopeStatus.AccessCodeCorrect); + 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 diff --git a/EnvelopeGenerator.Web/Controllers/Test/TestCacheController.cs b/EnvelopeGenerator.Web/Controllers/Test/TestCacheController.cs new file mode 100644 index 00000000..114aab85 --- /dev/null +++ b/EnvelopeGenerator.Web/Controllers/Test/TestCacheController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Distributed; + +namespace EnvelopeGenerator.Web.Controllers.Test +{ + [Route("api/[controller]")] + [ApiController] + public class TestCacheController : ControllerBase + { + private readonly IDistributedCache _cache; + + public TestCacheController(IDistributedCache cache) + { + _cache = cache; + } + + [HttpPost] + public async Task SetCacheAsync(string key, string value) + { + var options = new DistributedCacheEntryOptions() + .SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); + + await _cache.SetStringAsync(key, value, options); + + return Ok(); + } + + [HttpGet] + public async Task GetCacheAsync(string key) + { + var value = await _cache.GetStringAsync(key); + return value is null ? BadRequest() : Ok(value); + } + } +} diff --git a/EnvelopeGenerator.Web/Controllers/Test/TestMessagingController.cs b/EnvelopeGenerator.Web/Controllers/Test/TestMessagingController.cs new file mode 100644 index 00000000..b0d6ee7a --- /dev/null +++ b/EnvelopeGenerator.Web/Controllers/Test/TestMessagingController.cs @@ -0,0 +1,24 @@ +using EnvelopeGenerator.Application.Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace EnvelopeGenerator.Web.Controllers.Test +{ + [Route("api/[controller]")] + [ApiController] + public class TestMessagingController : ControllerBase + { + private readonly IMessagingService _service; + + public TestMessagingController(IMessagingService service) + { + _service = service; + } + + [HttpPost] + public async Task SendAsync(string recipient, string message, bool staticResponse = true) + { + var res = await _service.SendSmsAsync(recipient: recipient, message: message); + return res is null? StatusCode(StatusCodes.Status500InternalServerError) : Ok(res); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj index 0d04bfa5..97402542 100644 --- a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj +++ b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj @@ -5,7 +5,7 @@ enable enable EnvelopeGenerator.Web - 2.5.0.0 + 2.8.1 Digital Data GmbH Digital Data GmbH EnvelopeGenerator.Web @@ -13,8 +13,8 @@ digital data envelope generator web EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly. Assets\icon.ico - 2.5.0.0 - 2.5.0.0 + 2.8.1 + 2.8.1 Copyright © 2024 Digital Data GmbH. All rights reserved. @@ -46,8 +46,8 @@ - - + + @@ -57,6 +57,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -120,6 +121,12 @@ True \ + + PreserveNewest + + + PreserveNewest + diff --git a/EnvelopeGenerator.Web/Models/Auth.cs b/EnvelopeGenerator.Web/Models/Auth.cs new file mode 100644 index 00000000..c85733f8 --- /dev/null +++ b/EnvelopeGenerator.Web/Models/Auth.cs @@ -0,0 +1,15 @@ +namespace EnvelopeGenerator.Web.Models +{ + 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 HasAuthenticatorCode => AuthenticatorCode is not null; + + public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1; + + public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Program.cs b/EnvelopeGenerator.Web/Program.cs index c2c2a23d..fbf24996 100644 --- a/EnvelopeGenerator.Web/Program.cs +++ b/EnvelopeGenerator.Web/Program.cs @@ -1,4 +1,3 @@ -using DigitalData.UserManager.Infrastructure.Repositories; using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Application.Services; using EnvelopeGenerator.Web.Services; @@ -9,7 +8,6 @@ using NLog.Web; using DigitalData.Core.API; using Microsoft.AspNetCore.Authentication.Cookies; using EnvelopeGenerator.Web.Models; -using DigitalData.Core.DTO; using System.Text.Encodings.Web; using Ganss.Xss; using Microsoft.Extensions.Options; @@ -17,6 +15,7 @@ using EnvelopeGenerator.Application; using DigitalData.EmailProfilerDispatcher; using EnvelopeGenerator.Infrastructure; using EnvelopeGenerator.Web.Sanitizers; +using EnvelopeGenerator.Application.Extensions; var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); logger.Info("Logging initialized!"); @@ -81,6 +80,12 @@ try //AddEF Core dbcontext var connStr = config.GetConnectionString(Key.Default) ?? throw new InvalidOperationException("There is no default connection string in appsettings.json."); builder.Services.AddDbContext(options => options.UseSqlServer(connStr)); + builder.Services.AddDistributedSqlServerCache(options => + { + options.ConnectionString = connStr; + options.SchemaName = "dbo"; + options.TableName = "TBDD_CACHE"; + }); // Add envelope generator services builder.Services.AddEnvelopeGenerator(config); diff --git a/EnvelopeGenerator.Web/Scripts/create-sql-cache.bat b/EnvelopeGenerator.Web/Scripts/create-sql-cache.bat new file mode 100644 index 00000000..138f70e3 --- /dev/null +++ b/EnvelopeGenerator.Web/Scripts/create-sql-cache.bat @@ -0,0 +1,2 @@ +dotnet sql-cache create "CONNECTION_STRING" dbo TBDD_CACHE +pause \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Scripts/create-sql-cache.sql b/EnvelopeGenerator.Web/Scripts/create-sql-cache.sql new file mode 100644 index 00000000..77aa2261 --- /dev/null +++ b/EnvelopeGenerator.Web/Scripts/create-sql-cache.sql @@ -0,0 +1,23 @@ +USE [DD_ECM] +GO + +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE TABLE [dbo].[TBDD_CACHE]( + [Id] [nvarchar](449) NOT NULL, + [Value] [varbinary](max) NOT NULL, + [ExpiresAtTime] [datetimeoffset](7) NOT NULL, + [SlidingExpirationInSeconds] [bigint] NULL, + [AbsoluteExpiration] [datetimeoffset](7) NULL, +PRIMARY KEY CLUSTERED +( + [Id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + + diff --git a/EnvelopeGenerator.Web/Services/EnvelopeOldService.cs b/EnvelopeGenerator.Web/Services/EnvelopeOldService.cs index f8786918..8dcd4d61 100644 --- a/EnvelopeGenerator.Web/Services/EnvelopeOldService.cs +++ b/EnvelopeGenerator.Web/Services/EnvelopeOldService.cs @@ -98,13 +98,6 @@ namespace EnvelopeGenerator.Web.Services //if documenet_path_dmz is existing in config, replace the path with it var config = await _configService.ReadDefaultAsync(); - if (config.DocumentPathDmz is not null && config.DocumentPathDmz != string.Empty) - foreach (var doc in envelope.Documents) - { - doc.Filepath = doc.Filepath.Replace(config.DocumentPath, config.DocumentPathDmz); - } - - return new() { Receiver = receiver, diff --git a/EnvelopeGenerator.Web/Views/Home/EnvelopeExpired.cshtml b/EnvelopeGenerator.Web/Views/Home/EnvelopeExpired.cshtml index 7ba52d25..843acfa4 100644 --- a/EnvelopeGenerator.Web/Views/Home/EnvelopeExpired.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/EnvelopeExpired.cshtml @@ -20,5 +20,4 @@

Der Zeitraum für die gemeinsame Nutzung von Dokumenten ist abgelaufen.

- - \ No newline at end of file + \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml b/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml index 6738e567..ab2eb857 100644 --- a/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml @@ -1,8 +1,21 @@ -@{ +@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; + string codeType = ViewData["CodeType"] is string _codeType ? _codeType : "accessCode"; + string codePropName = char.ToUpper(codeType[0]) + codeType.Substring(1); + string codeKeyName = codePropName.Replace("Code", ""); + bool viaSms = codeType == "smsCode"; + bool viaAuthenticator = codeType == "authenticatorCode"; + bool viaTFA = viaSms || viaAuthenticator; + DateTime? smsExpiration = ViewData["SmsExpiration"] is DateTime _smsExpiration ? _smsExpiration : null; + DateTime? qrCodeExpiration = ViewData["QRCodeExpiration"] is DateTime _qrCodeExpiration ? _qrCodeExpiration : null; + bool tfaEnabled = ViewData["TFAEnabled"] is bool _tfaEnabled && _tfaEnabled; + bool hasPhoneNumber = ViewData["HasPhoneNumber"] is bool _hasPhoneNumber && _hasPhoneNumber; }
@@ -10,28 +23,46 @@

@_localizer[WebKey.WelcomeToTheESignPortal]

-
+
-

@_localizer[WebKey.LockedTitle]

+

@_localizer[WebKey.Formats.LockedTitle.Format(codeKeyName)]

-

@_localizer[WebKey.LockedBody]

+

@_localizer[WebKey.Formats.LockedBody.Format(codeKeyName)].Value.Format(qrCodeExpiration.ToString())

- - + + + @if (tfaEnabled) + { +
+ @if(hasPhoneNumber) + { + + } + else + { + + } + +
+ } + @if (smsExpiration is not null) + { + + }
@@ -45,8 +76,34 @@ }
- @_localizer[WebKey.LockedFooterTitle] -

@_localizer[WebKey.LockedFooterBody]

+ @_localizer[WebKey.Formats.LockedFooterTitle.Format(codeKeyName)] +

@_localizer[WebKey.Formats.LockedFooterBody.Format(codeKeyName)]

-
\ No newline at end of file +
+ \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Views/Home/EnvelopeRejected.cshtml b/EnvelopeGenerator.Web/Views/Home/EnvelopeRejected.cshtml index 8ba4a6b4..f7663271 100644 --- a/EnvelopeGenerator.Web/Views/Home/EnvelopeRejected.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/EnvelopeRejected.cshtml @@ -68,5 +68,4 @@

- - \ No newline at end of file + \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Views/Home/EnvelopeSigned.cshtml b/EnvelopeGenerator.Web/Views/Home/EnvelopeSigned.cshtml index 5d0d7379..67cd10ff 100644 --- a/EnvelopeGenerator.Web/Views/Home/EnvelopeSigned.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/EnvelopeSigned.cshtml @@ -14,5 +14,4 @@

Sie haben das Dokument signiert. Im Anschluss erhalten Sie eine schriftliche Bestätigung.

- - \ No newline at end of file + \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml b/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml index f92297f5..62cd7266 100644 --- a/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml @@ -29,7 +29,7 @@ @if (!isReadOnly) {
-
- Datenschutz + @_localizer[WebKey.Privacy] \ No newline at end of file diff --git a/EnvelopeGenerator.Web/WebKey.cs b/EnvelopeGenerator.Web/WebKey.cs index d87c179e..e039ba05 100644 --- a/EnvelopeGenerator.Web/WebKey.cs +++ b/EnvelopeGenerator.Web/WebKey.cs @@ -10,12 +10,6 @@ public static readonly string NonDecodableEnvelopeReceiverId = nameof(NonDecodableEnvelopeReceiverId); public static readonly string de_DE = nameof(de_DE).Replace("_", "-"); public static readonly string en_US = nameof(en_US).Replace("_", "-"); - public static readonly string LockedTitle = nameof(LockedTitle); - public static readonly string LockedBody = nameof(LockedBody); - public static readonly string LocakedOpen = nameof(LocakedOpen); - public static readonly string LockedAccessCode = nameof(LockedAccessCode); - public static readonly string LockedFooterTitle = nameof(LockedFooterTitle); - public static readonly string LockedFooterBody = nameof(LockedFooterBody); public static readonly string WrongAccessCode = nameof(WrongAccessCode); public static readonly string SignDoc = nameof(SignDoc); public static readonly string DocRejected = nameof(DocRejected); @@ -36,5 +30,23 @@ public static readonly string WelcomeToTheESignPortal = nameof(WelcomeToTheESignPortal); public static readonly string ViewDoc = nameof(ViewDoc); public static readonly string HomePageDescription = nameof(HomePageDescription); + public static readonly string Privacy = nameof(Privacy); + + public static class Formats + { + public static readonly string LockedTitle = nameof(LockedTitle) + "{0}"; + + public static readonly string LockedBody = nameof(LockedBody) + "{0}"; + + public static readonly string LockedCodeLabel = nameof(LockedCodeLabel) + "{0}"; + + public static readonly string LockedFooterTitle = nameof(LockedFooterTitle) + "{0}"; + + public static readonly string LockedFooterBody = nameof(LockedFooterBody) + "{0}"; + } + + public static string Format(this string st, object? arg0) => string.Format(st, arg0: arg0); + + public static string Format(this string st, params object?[] args) => string.Format(st, args: args); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/appsettings.json b/EnvelopeGenerator.Web/appsettings.json index 7f37fbf3..6c4b4e50 100644 --- a/EnvelopeGenerator.Web/appsettings.json +++ b/EnvelopeGenerator.Web/appsettings.json @@ -125,5 +125,18 @@ "[SIGNATURE_TYPE]": "signieren", "[REASON]": "" } - } + }, + "GTXMessagingConfig": { + "AuthKey": "ep$?A!Gs" + }, + "SmsConfig": { + "Uri": "https://rest.gtx-messaging.net", + "Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json", + "Headers": {}, + "QueryParams": { + "from": "signFlow" + }, + "CodeCacheValidityPeriod": "00:10:00" + }, + "EnvelopeReceiverCacheParams": {} } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/wwwroot/css/site.css b/EnvelopeGenerator.Web/wwwroot/css/site.css index 90f162d4..00fa9a6d 100644 --- a/EnvelopeGenerator.Web/wwwroot/css/site.css +++ b/EnvelopeGenerator.Web/wwwroot/css/site.css @@ -221,10 +221,15 @@ footer { } .page header .icon.locked { - background-color: #ffc107; + background-color: #ffa407; color: #000; } + .page header .icon.locked.tfa { + background-color: #ff7207; + color: #000; + } + .page header .icon.signed { background-color: #146c43; color: #fff; @@ -397,14 +402,17 @@ footer#page-footer { .access-code-form-floating { display: flex; - justify-content: start; + justify-content: space-between; flex-direction: row; + align-items: center; } .access-code-form-floating button { align-content: center; border-bottom-left-radius: 0; border-top-left-radius: 0; + margin:0; + height: 100%; } .access-code-form-floating input { @@ -422,6 +430,34 @@ footer#page-footer { height: 2.5rem; } +#sms-timer { + height: 3rem; + display: flex; + align-items: center; + font-family: 'Arial', sans-serif; + font-weight: bold; + color: #ffffff; + background-color: #007bff; + margin: 0 0 0 2rem; + border-radius: 8px; + text-align: center; +} + + #sms-timer:hover { + background-color: #0056b3; + 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%; }*/ diff --git a/EnvelopeGenerator.Web/wwwroot/css/site.min.css b/EnvelopeGenerator.Web/wwwroot/css/site.min.css index d10c4ed6..4a2cdb24 100644 --- a/EnvelopeGenerator.Web/wwwroot/css/site.min.css +++ b/EnvelopeGenerator.Web/wwwroot/css/site.min.css @@ -1 +1 @@ -.navbar-toggler{border:0}.material-symbols-outlined{align-content:center}.btn-group{margin-right:10vw;margin-bottom:10vh}.btn_refresh,.btn_reject,.btn_complete{height:2.5rem}.btn_complete .icon,.btn_reject .icon,.btn_refresh .icon{width:1.1rem}.btn_complete span,.btn_reject span,.btn_refresh span{vertical-align:middle}.button-finish{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.button-finish:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.button-finish:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.button-finish:active{color:#fff;background-color:#0a58ca;border-color:#0a53be;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.button-finish:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.button-reject{color:#fff;background-color:#dc3545;border-color:#dc3545}.button-reject:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.button-reject:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.button-reject:active{color:#fff;background-color:#b02a37;border-color:#a52834;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.button-reject:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.button-reset{color:#fff;background-color:#6c757d;border-color:#6c757d}.button-reset:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.button-reset:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.button-reset:active{color:#fff;background-color:#565e64;border-color:#51585e;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.button-reset:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}body{background:#f8fcfc;display:flex;flex-direction:column;height:100vh;margin:0}main{display:flex;margin:0 0 .5vh 0}.home-description{text-align:justify;font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;font-weight:500;font-size:.95em;letter-spacing:-1px;word-spacing:-2px}.envelope-view{display:flex;flex-direction:column;width:100vw;height:95.9vh}#app{background:#808080;width:100vw;height:100%;flex-grow:1;border-width:0}footer{height:4vh;min-height:1.5rem;background-color:#001f61;border-radius:10px 10px 0 0;color:#fff;font-family:'Muli';padding:.5vh 0;position:fixed;bottom:0;width:100%;z-index:998;border-width:0;font-size:clamp(.58rem,1.5vw,1rem);display:flex;flex-direction:row;justify-content:space-around;align-items:center}footer *{margin-left:clamp(.5rem,2vw,1rem)}footer a{color:#ff7500;text-decoration:none}footer .dropdown-toggle,footer .flag-dropdown,footer li{margin:0;padding:0;border-width:0}footer .dropdown-menu a{padding:.25rem 1rem .25rem 1rem;margin-left:0;user-select:none}.page{margin-top:3rem;background:#fff;border-radius:.313rem;box-shadow:rgba(9,30,66,.25) 0 .25rem .5rem -.125rem,rgba(9,30,66,.08) 0 0 0 .063rem;max-width:40rem}.page section{max-width:30rem;margin:0 auto}.page header .icon{display:inline-block;border-radius:6.25rem;padding:.938rem;margin-bottom:2rem}.page header .icon.admin{background-color:#331904;color:#fecba1}.page header .icon.locked{background-color:#ffc107;color:#000}.page header .icon.signed{background-color:#146c43;color:#fff}.page header .icon.rejected{background-color:#e4d8d5;color:#fff}.page header .icon.expired{background-color:rgba(228,216,213,.5);color:#fff}.page .form{max-width:30rem;margin:2rem auto;display:flex;gap:1rem}#form-access-code>.input,#form-admin-password>.input{flex-grow:1}#page-admin header .icon{background-color:#331904;color:#fecba1}.envelope{display:block;border:.063rem solid #eee;margin-bottom:1rem;padding:.5rem}footer#page-footer{color:#333;max-width:40rem;margin-top:1rem;font-size:.85rem}footer#page-footer a,footer#page-footer a:link,footer#page-footer a:hover,footer#page-footer a:visited,footer#page-footer a:focus{color:#444}.sender-card{background-color:transparent;border:0}.sender-card .row{height:7vh}.sender-card img{height:7vh;background-color:#d1cfcf;border-radius:3.125rem}.navbar .container{display:flex;padding:0;margin:0}.navbar-toggler{padding:0;margin:0;width:4rem;left:0}.envelope-message{position:absolute;display:flex;width:calc(100% - 8rem);align-items:center;justify-content:start;margin-left:4rem}.envelope-message .icon{margin-right:.5rem}.envelope-message .message{font-family:'Roboto',sans-serif;font-size:16px;font-weight:550}.logo{width:9rem;position:absolute;right:0;margin-right:2rem}.none-display{display:none}.dropdown-flag img,.img-flag{width:30%;height:70%}.dropdown-flag{height:75%;width:75%}.increase-dropdown-height{min-height:25rem}.dropdown-flag .select2-container{width:100%!important;max-width:11.25rem}.lang-item{font-size:.85rem}#langDropdownMenuButton{min-width:4vw}.highlight{font-weight:700;font-size:.85rem}.signature-process-title,.signature-process-name{font-size:1.125rem}.mail-link{color:#000;text-decoration:none}.mail-link:hover{text-decoration:underline}#flex-action-panel{z-index:1050}#form-access-code{justify-content:space-evenly}.access-code-form-floating{display:flex;justify-content:start;flex-direction:row}.access-code-form-floating button{align-content:center;border-bottom-left-radius:0;border-top-left-radius:0}.access-code-form-floating input{align-content:center;border-bottom-right-radius:0;border-top-right-radius:0;border-right-width:0;width:7rem}#access-code-error-message{justify-content:center;align-content:center;margin:1.5rem 7rem 0 7rem;height:2.5rem}.header-1{align-items:center;justify-content:space-between;margin-top:0;padding-top:0}.header-1 .text{text-align:center;margin-left:1.5vw;margin-top:0;padding-top:0}.no-receiver-explanation{padding:2.5rem}.ajs-message.ajs-custom{margin:0 0 0 0;padding:0 0 0 0;width:50rem}.ajs-message.ajs-custom .alert{display:flex;flex-direction:row}.ajs-message.ajs-custom span{margin:0 1rem 0 0}.ajs-message.ajs-custom p{margin:0;padding:0}@media(max-height:850px){.navbar .container{display:flex;padding:0;margin:0}.navbar-toggler{padding:0;margin:0;width:4rem;left:0}.envelope-message{width:calc(100% - 4rem - 9rem)}.envelope-message .message{font-size:14px;font-weight:550}.logo{width:9rem;position:absolute;right:0}.card-text,.card-text{font-size:.6rem;margin:0;padding:0}.highlight{font-weight:700;font-size:.5rem}.signature-process-title,.signature-process-name{font-size:.7rem}}@media(max-width:767px){.navbar{flex-direction:column;align-items:flex-start}.navbar-brand{font-size:.5rem;text-align:center;overflow:hidden;text-overflow:ellipsis}.envelope-message{width:calc(100% - 4rem - 4.5rem);margin-left:3rem}.envelope-message .message{font-size:12px;font-weight:550}.envelope-message .icon{margin-right:.1rem;font-size:1rem}.logo{width:5rem;right:0;margin-right:1rem}.btn_group{position:fixed;flex-direction:row;bottom:.5rem;right:.5rem}.img-fluid{width:1.2rem;height:100%;display:none}.page{margin-top:1rem;max-width:90%;padding:.5rem}.page section{max-width:90%}#form-access-code{margin-left:0}}@media(max-width:1024px){#flex-action-panel,.btn-desktop{display:none}}@media(max-height:600px){.collapse{height:4rem}} \ No newline at end of file +.navbar-toggler{border:0}.material-symbols-outlined{align-content:center}.btn-group{margin-right:10vw;margin-bottom:10vh}.btn_refresh,.btn_reject,.btn_complete{height:2.5rem}.btn_complete .icon,.btn_reject .icon,.btn_refresh .icon{width:1.1rem}.btn_complete span,.btn_reject span,.btn_refresh span{vertical-align:middle}.button-finish{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.button-finish:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.button-finish:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.button-finish:active{color:#fff;background-color:#0a58ca;border-color:#0a53be;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.button-finish:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.button-reject{color:#fff;background-color:#dc3545;border-color:#dc3545}.button-reject:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.button-reject:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.button-reject:active{color:#fff;background-color:#b02a37;border-color:#a52834;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.button-reject:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.button-reset{color:#fff;background-color:#6c757d;border-color:#6c757d}.button-reset:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.button-reset:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.button-reset:active{color:#fff;background-color:#565e64;border-color:#51585e;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.button-reset:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}body{background:#f8fcfc;display:flex;flex-direction:column;height:100vh;margin:0}main{display:flex;margin:0 0 .5vh 0}.home-description{text-align:justify;font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;font-weight:500;font-size:.95em;letter-spacing:-1px;word-spacing:-2px}.envelope-view{display:flex;flex-direction:column;width:100vw;height:95.9vh}#app{background:#808080;width:100vw;height:100%;flex-grow:1;border-width:0}footer{height:4vh;min-height:1.5rem;background-color:#001f61;border-radius:10px 10px 0 0;color:#fff;font-family:'Muli';padding:.5vh 0;position:fixed;bottom:0;width:100%;z-index:998;border-width:0;font-size:clamp(.58rem,1.5vw,1rem);display:flex;flex-direction:row;justify-content:space-around;align-items:center}footer *{margin-left:clamp(.5rem,2vw,1rem)}footer a{color:#ff7500;text-decoration:none}footer .dropdown-toggle,footer .flag-dropdown,footer li{margin:0;padding:0;border-width:0}footer .dropdown-menu a{padding:.25rem 1rem .25rem 1rem;margin-left:0;user-select:none}.page{margin-top:3rem;background:#fff;border-radius:.313rem;box-shadow:rgba(9,30,66,.25) 0 .25rem .5rem -.125rem,rgba(9,30,66,.08) 0 0 0 .063rem;max-width:40rem}.page section{max-width:30rem;margin:0 auto}.page header .icon{display:inline-block;border-radius:6.25rem;padding:.938rem;margin-bottom:2rem}.page header .icon.admin{background-color:#331904;color:#fecba1}.page header .icon.locked{background-color:#ffa407;color:#000}.page header .icon.locked.tfa{background-color:#ff7207;color:#000}.page header .icon.signed{background-color:#146c43;color:#fff}.page header .icon.rejected{background-color:#e4d8d5;color:#fff}.page header .icon.expired{background-color:rgba(228,216,213,.5);color:#fff}.page .form{max-width:30rem;margin:2rem auto;display:flex;gap:1rem}#form-access-code>.input,#form-admin-password>.input{flex-grow:1}#page-admin header .icon{background-color:#331904;color:#fecba1}.envelope{display:block;border:.063rem solid #eee;margin-bottom:1rem;padding:.5rem}footer#page-footer{color:#333;max-width:40rem;margin-top:1rem;font-size:.85rem}footer#page-footer a,footer#page-footer a:link,footer#page-footer a:hover,footer#page-footer a:visited,footer#page-footer a:focus{color:#444}.sender-card{background-color:transparent;border:0}.sender-card .row{height:7vh}.sender-card img{height:7vh;background-color:#d1cfcf;border-radius:3.125rem}.navbar .container{display:flex;padding:0;margin:0}.navbar-toggler{padding:0;margin:0;width:4rem;left:0}.envelope-message{position:absolute;display:flex;width:calc(100% - 8rem);align-items:center;justify-content:start;margin-left:4rem}.envelope-message .icon{margin-right:.5rem}.envelope-message .message{font-family:'Roboto',sans-serif;font-size:16px;font-weight:550}.logo{width:9rem;position:absolute;right:0;margin-right:2rem}.none-display{display:none}.dropdown-flag img,.img-flag{width:30%;height:70%}.dropdown-flag{height:75%;width:75%}.increase-dropdown-height{min-height:25rem}.dropdown-flag .select2-container{width:100%!important;max-width:11.25rem}.lang-item{font-size:.85rem}#langDropdownMenuButton{min-width:4vw}.highlight{font-weight:700;font-size:.85rem}.signature-process-title,.signature-process-name{font-size:1.125rem}.mail-link{color:#000;text-decoration:none}.mail-link:hover{text-decoration:underline}#flex-action-panel{z-index:1050}#form-access-code{justify-content:space-evenly}.access-code-form-floating{display:flex;justify-content:space-between;flex-direction:row;align-items:center}.access-code-form-floating button{align-content:center;border-bottom-left-radius:0;border-top-left-radius:0;margin:0;height:100%}.access-code-form-floating input{align-content:center;border-bottom-right-radius:0;border-top-right-radius:0;border-right-width:0;width:7rem}#access-code-error-message{justify-content:center;align-content:center;margin:1.5rem 7rem 0 7rem;height:2.5rem}#sms-timer{height:3rem;display:flex;align-items:center;font-family:'Arial',sans-serif;font-weight:bold;color:#fff;background-color:#007bff;margin:0 0 0 2rem;border-radius:8px;text-align:center}#sms-timer:hover{background-color:#0056b3;cursor:pointer}.form-check.tfa-sms{margin-left:2rem}.form-check.tfa-sms .form-check-label{font-size:.875rem;font-weight:500;margin-left:-.1rem}.header-1{align-items:center;justify-content:space-between;margin-top:0;padding-top:0}.header-1 .text{text-align:center;margin-left:1.5vw;margin-top:0;padding-top:0}.no-receiver-explanation{padding:2.5rem}.ajs-message.ajs-custom{margin:0 0 0 0;padding:0 0 0 0;width:50rem}.ajs-message.ajs-custom .alert{display:flex;flex-direction:row}.ajs-message.ajs-custom span{margin:0 1rem 0 0}.ajs-message.ajs-custom p{margin:0;padding:0}@media(max-height:850px){.navbar .container{display:flex;padding:0;margin:0}.navbar-toggler{padding:0;margin:0;width:4rem;left:0}.envelope-message{width:calc(100% - 4rem - 9rem)}.envelope-message .message{font-size:14px;font-weight:550}.logo{width:9rem;position:absolute;right:0}.card-text,.card-text{font-size:.6rem;margin:0;padding:0}.highlight{font-weight:700;font-size:.5rem}.signature-process-title,.signature-process-name{font-size:.7rem}}@media(max-width:767px){.navbar{flex-direction:column;align-items:flex-start}.navbar-brand{font-size:.5rem;text-align:center;overflow:hidden;text-overflow:ellipsis}.envelope-message{width:calc(100% - 4rem - 4.5rem);margin-left:3rem}.envelope-message .message{font-size:12px;font-weight:550}.envelope-message .icon{margin-right:.1rem;font-size:1rem}.logo{width:5rem;right:0;margin-right:1rem}.btn_group{position:fixed;flex-direction:row;bottom:.5rem;right:.5rem}.img-fluid{width:1.2rem;height:100%;display:none}.page{margin-top:1rem;max-width:90%;padding:.5rem}.page section{max-width:90%}#form-access-code{margin-left:0}}@media(max-width:1024px){#flex-action-panel,.btn-desktop{display:none}}@media(max-height:600px){.collapse{height:4rem}} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/wwwroot/js/annotation.js b/EnvelopeGenerator.Web/wwwroot/js/annotation.js index 0e5d8e32..82faac8b 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/annotation.js +++ b/EnvelopeGenerator.Web/wwwroot/js/annotation.js @@ -82,7 +82,6 @@ }) //city - var location = await getLocation(); const id_city = PSPDFKit.generateInstantId() const annotation_city = new PSPDFKit.Annotations.WidgetAnnotation({ id: id_city, @@ -102,8 +101,8 @@ const formFieldCity = new PSPDFKit.FormFields.TextFormField({ name: id_city, annotationIds: PSPDFKit.Immutable.List([annotation_city.id]), - value: IS_MOBILE_DEVICE ? location.city : "", - readOnly: IS_MOBILE_DEVICE + value: "", + readOnly: false }) this.markFieldAsRequired(formFieldCity); diff --git a/EnvelopeGenerator.Web/wwwroot/js/event-binder.js b/EnvelopeGenerator.Web/wwwroot/js/event-binder.js index d36137ed..04c1a5c9 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/event-binder.js +++ b/EnvelopeGenerator.Web/wwwroot/js/event-binder.js @@ -11,7 +11,7 @@ document.querySelectorAll('.email-input').forEach(input => { document.addEventListener('DOMContentLoaded', function () { var dropdownItems = document.querySelectorAll('.culture-dropdown-item'); dropdownItems.forEach(function (item) { - item.addEventListener('click', async function(event) { + item.addEventListener('click', async function (event) { event.preventDefault(); var language = this.getAttribute('data-language'); var flagCode = this.getAttribute('data-flag'); @@ -21,6 +21,30 @@ document.addEventListener('DOMContentLoaded', function () { }); }); +const setTimer = (elementId, expirationTime) => { + const element = document.getElementById(elementId); + + const interval = setInterval(function () { + var now = new Date(); + + var diffInMillis = expirationTime - now; + + if (diffInMillis <= 0) { + element.textContent = "00:00"; + clearInterval(interval); + } + + var minutes = Math.floor(diffInMillis / 1000 / 60); + var seconds = Math.floor((diffInMillis / 1000) % 60); + + var formattedMinutes = minutes.toString().padStart(2, '0'); + var formattedSeconds = seconds.toString().padStart(2, '0'); + + var remainingTime = `${formattedMinutes}:${formattedSeconds}`; + element.textContent = remainingTime; + }, 1000); +} + const bsNotify = (message, options) => alertify.notify( ``, 'custom', diff --git a/EnvelopeGenerator.Web/wwwroot/js/event-binder.min.js b/EnvelopeGenerator.Web/wwwroot/js/event-binder.min.js index 30f6d827..d56f9789 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/event-binder.min.js +++ b/EnvelopeGenerator.Web/wwwroot/js/event-binder.min.js @@ -1 +1 @@ -document.querySelectorAll(".email-input").forEach(n=>{n.addEventListener("input",function(){/^\S+@\S+\.\S+$/.test(this.value)?this.classList.remove("is-invalid"):this.classList.add("is-invalid")})});document.addEventListener("DOMContentLoaded",function(){var n=document.querySelectorAll(".culture-dropdown-item");n.forEach(function(n){n.addEventListener("click",async function(n){n.preventDefault();var t=this.getAttribute("data-language"),i=this.getAttribute("data-flag");document.getElementById("selectedFlag").className="fi "+i+" me-2";await setLanguage(t)})})});const bsNotify=(n,t)=>alertify.notify(``,"custom",t?.delay??5);class Comp{static ActPanel=class{static __Root;static get Root(){Comp.ActPanel.__Root??=document.getElementById("flex-action-panel");return Comp.ActPanel.__Root}static get Elements(){return[...Comp.ActPanel.Root.children]}static get IsHided(){return Comp.ActPanel.Root.style.display=="none"}static set Display(n){Comp.ActPanel.Root.style.display=n;Comp.ActPanel.Elements.forEach(t=>t.style.display=n)}static Toggle(){Comp.ActPanel.Display=Comp.ActPanel.IsHided?"":"none"}};static SignatureProgress=class{static __SignatureCount;static get SignatureCount(){this.__SignatureCount=parseInt(document.getElementById("signature-count").innerText);return this.__SignatureCount}static __SignedCountSpan;static get SignedCountSpan(){this.__SignedCountSpan??=document.getElementById("signed-count");return Comp.SignatureProgress.__SignedCountSpan}static __signedCount=0;static get SignedCount(){return this.__signedCount}static set SignedCount(n){this.__signedCount=n;const t=(n/this.SignatureCount)*100;this.SignedCountBar.style.setProperty("--progress-width",t+"%");this.SignedCountSpan.innerText=n.toString()}static __SignedCountBar;static get SignedCountBar(){this.__SignedCountBar??=document.getElementById("signed-count-bar");return this.__SignedCountBar}};static __ShareBackdrop;static get ShareBackdrop(){return Comp.__ShareBackdrop??=new bootstrap.Modal(document.getElementById("shareBackdrop")),this.__ShareBackdrop}} \ No newline at end of file +document.querySelectorAll(".email-input").forEach(n=>{n.addEventListener("input",function(){/^\S+@\S+\.\S+$/.test(this.value)?this.classList.remove("is-invalid"):this.classList.add("is-invalid")})});document.addEventListener("DOMContentLoaded",function(){var n=document.querySelectorAll(".culture-dropdown-item");n.forEach(function(n){n.addEventListener("click",async function(n){n.preventDefault();var t=this.getAttribute("data-language"),i=this.getAttribute("data-flag");document.getElementById("selectedFlag").className="fi "+i+" me-2";await setLanguage(t)})})});const setTimer=(n,t)=>{const i=document.getElementById(n),r=setInterval(function(){var u=new Date,n=t-u;n<=0&&(i.textContent="00:00",clearInterval(r));var f=Math.floor(n/6e4),e=Math.floor(n/1e3%60),o=f.toString().padStart(2,"0"),s=e.toString().padStart(2,"0"),h=`${o}:${s}`;i.textContent=h},1e3)},bsNotify=(n,t)=>alertify.notify(``,"custom",t?.delay??5);class Comp{static ActPanel=class{static __Root;static get Root(){Comp.ActPanel.__Root??=document.getElementById("flex-action-panel");return Comp.ActPanel.__Root}static get Elements(){return[...Comp.ActPanel.Root.children]}static get IsHided(){return Comp.ActPanel.Root.style.display=="none"}static set Display(n){Comp.ActPanel.Root.style.display=n;Comp.ActPanel.Elements.forEach(t=>t.style.display=n)}static Toggle(){Comp.ActPanel.Display=Comp.ActPanel.IsHided?"":"none"}};static SignatureProgress=class{static __SignatureCount;static get SignatureCount(){this.__SignatureCount=parseInt(document.getElementById("signature-count").innerText);return this.__SignatureCount}static __SignedCountSpan;static get SignedCountSpan(){this.__SignedCountSpan??=document.getElementById("signed-count");return Comp.SignatureProgress.__SignedCountSpan}static __signedCount=0;static get SignedCount(){return this.__signedCount}static set SignedCount(n){this.__signedCount=n;const t=(n/this.SignatureCount)*100;this.SignedCountBar.style.setProperty("--progress-width",t+"%");this.SignedCountSpan.innerText=n.toString()}static __SignedCountBar;static get SignedCountBar(){this.__SignedCountBar??=document.getElementById("signed-count-bar");return this.__SignedCountBar}};static __ShareBackdrop;static get ShareBackdrop(){return Comp.__ShareBackdrop??=new bootstrap.Modal(document.getElementById("shareBackdrop")),this.__ShareBackdrop}} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/wwwroot/js/util.js b/EnvelopeGenerator.Web/wwwroot/js/util.js index 90d0f5be..5663930a 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/util.js +++ b/EnvelopeGenerator.Web/wwwroot/js/util.js @@ -1,50 +1,5 @@ const B64ToBuff = (base64String) => new Uint8Array(Array.from(atob(base64String), char => char.charCodeAt(0))).buffer; -function getCoordinates() { - return new Promise((resolve, reject) => { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - position => resolve(position.coords), - error => reject(error) - ); - } else { - reject(new Error("Geolocation is not supported by this browser.")); - } - }); -} - -async function getCity() { - try { - const coords = await getCoordinates(); - const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${coords.latitude}&lon=${coords.longitude}`); - const data = await response.json(); - - if (data && data.address) { - const city = data.address.city || data.address.town || data.address.village || data.address.hamlet; - const postalCode = data.address.postcode; - return postalCode + ' ' + city || ''; - } - } catch { - return ''; - } -} - -async function getLocation() { - try { - const coords = await getCoordinates(); - const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${coords.latitude}&lon=${coords.longitude}`); - const data = await response.json(); - - if (data && data.address) { - const city = data.address.city || data.address.town || data.address.village || data.address.hamlet; - const postalCode = data.address.postcode; - return { postalCode: postalCode, city: city }; - } - } catch { - return { postalCode: '', city: '' }; - } -} - const getLocaleDateString = _ => new Date().toLocaleDateString('de-DE') function detailedCurrentDate() { diff --git a/EnvelopeGenerator.Web/wwwroot/js/util.location.js b/EnvelopeGenerator.Web/wwwroot/js/util.location.js new file mode 100644 index 00000000..c7fcda2f --- /dev/null +++ b/EnvelopeGenerator.Web/wwwroot/js/util.location.js @@ -0,0 +1,44 @@ +function getCoordinates() { + return new Promise((resolve, reject) => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + position => resolve(position.coords), + error => reject(error) + ); + } else { + reject(new Error("Geolocation is not supported by this browser.")); + } + }); +} + +async function getCity() { + try { + const coords = await getCoordinates(); + const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${coords.latitude}&lon=${coords.longitude}`); + const data = await response.json(); + + if (data && data.address) { + const city = data.address.city || data.address.town || data.address.village || data.address.hamlet; + const postalCode = data.address.postcode; + return postalCode + ' ' + city || ''; + } + } catch { + return ''; + } +} + +async function getLocation() { + try { + const coords = await getCoordinates(); + const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${coords.latitude}&lon=${coords.longitude}`); + const data = await response.json(); + + if (data && data.address) { + const city = data.address.city || data.address.town || data.address.village || data.address.hamlet; + const postalCode = data.address.postcode; + return { postalCode: postalCode, city: city }; + } + } catch { + return { postalCode: '', city: '' }; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/wwwroot/js/util.min.js b/EnvelopeGenerator.Web/wwwroot/js/util.min.js index e70e8b90..bf9b7d7f 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/util.min.js +++ b/EnvelopeGenerator.Web/wwwroot/js/util.min.js @@ -1 +1 @@ -function getCoordinates(){return new Promise((n,t)=>{navigator.geolocation?navigator.geolocation.getCurrentPosition(t=>n(t.coords),n=>t(n)):t(new Error("Geolocation is not supported by this browser."))})}async function getCity(){try{const t=await getCoordinates(),i=await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${t.latitude}&lon=${t.longitude}`),n=await i.json();if(n&&n.address){const t=n.address.city||n.address.town||n.address.village||n.address.hamlet,i=n.address.postcode;return i+" "+t||""}}catch{return""}}async function getLocation(){try{const t=await getCoordinates(),i=await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${t.latitude}&lon=${t.longitude}`),n=await i.json();if(n&&n.address){const t=n.address.city||n.address.town||n.address.village||n.address.hamlet,i=n.address.postcode;return{postalCode:i,city:t}}}catch{return{postalCode:"",city:""}}}function detailedCurrentDate(){return new Intl.DateTimeFormat("de-DE",{day:"2-digit",month:"2-digit",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"shortOffset"}).format()}const B64ToBuff=n=>new Uint8Array(Array.from(atob(n),n=>n.charCodeAt(0))).buffer;const getLocaleDateString=()=>(new Date).toLocaleDateString("de-DE"); \ No newline at end of file +function detailedCurrentDate(){return new Intl.DateTimeFormat("de-DE",{day:"2-digit",month:"2-digit",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"shortOffset"}).format()}const B64ToBuff=n=>new Uint8Array(Array.from(atob(n),n=>n.charCodeAt(0))).buffer,getLocaleDateString=()=>(new Date).toLocaleDateString("de-DE"); \ No newline at end of file diff --git a/EnvelopeGenerator.sln b/EnvelopeGenerator.sln index 900a5ca6..e5051c8f 100644 --- a/EnvelopeGenerator.sln +++ b/EnvelopeGenerator.sln @@ -21,7 +21,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Applicati EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.GeneratorAPI", "EnvelopeGenerator.GeneratorAPI\EnvelopeGenerator.GeneratorAPI.csproj", "{E5E12BA4-60C1-48BA-9053-0F8B62B38124}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Extensions", "EnvelopeGenerator.Extensions\EnvelopeGenerator.Extensions.csproj", "{47F98812-4280-4D53-B04A-2AAEEA5EBC31}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Extensions", "EnvelopeGenerator.Extensions\EnvelopeGenerator.Extensions.csproj", "{47F98812-4280-4D53-B04A-2AAEEA5EBC31}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,44 +31,44 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {089D5634-FB6B-42D0-B912-7AA7457044E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {089D5634-FB6B-42D0-B912-7AA7457044E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.Build.0 = Release|Any CPU - {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Release|Any CPU.Build.0 = Release|Any CPU + {089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.Build.0 = Debug|Any CPU + {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.Build.0 = Release|Any CPU + {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Release|Any CPU.Build.0 = Debug|Any CPU {6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Release|Any CPU.Build.0 = Release|Any CPU + {6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Release|Any CPU.Build.0 = Debug|Any CPU {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.Build.0 = Release|Any CPU + {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.Build.0 = Debug|Any CPU {83ED2617-B398-4859-8F59-B38F8807E83E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {83ED2617-B398-4859-8F59-B38F8807E83E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.Build.0 = Release|Any CPU + {83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.Build.0 = Debug|Any CPU {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Release|Any CPU.Build.0 = Release|Any CPU + {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Release|Any CPU.Build.0 = Debug|Any CPU {63E32615-0ECA-42DC-96E3-91037324B7C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {63E32615-0ECA-42DC-96E3-91037324B7C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63E32615-0ECA-42DC-96E3-91037324B7C7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63E32615-0ECA-42DC-96E3-91037324B7C7}.Release|Any CPU.Build.0 = Release|Any CPU + {63E32615-0ECA-42DC-96E3-91037324B7C7}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {63E32615-0ECA-42DC-96E3-91037324B7C7}.Release|Any CPU.Build.0 = Debug|Any CPU {5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Release|Any CPU.Build.0 = Release|Any CPU + {5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Release|Any CPU.Build.0 = Debug|Any CPU {E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.Build.0 = Release|Any CPU + {E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.Build.0 = Debug|Any CPU {47F98812-4280-4D53-B04A-2AAEEA5EBC31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47F98812-4280-4D53-B04A-2AAEEA5EBC31}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47F98812-4280-4D53-B04A-2AAEEA5EBC31}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47F98812-4280-4D53-B04A-2AAEEA5EBC31}.Release|Any CPU.Build.0 = Release|Any CPU + {47F98812-4280-4D53-B04A-2AAEEA5EBC31}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {47F98812-4280-4D53-B04A-2AAEEA5EBC31}.Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE