diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadEnvelopeReceiverSecretQuery.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadEnvelopeReceiverSecretQuery.cs new file mode 100644 index 00000000..e9fd1c20 --- /dev/null +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadEnvelopeReceiverSecretQuery.cs @@ -0,0 +1,127 @@ +using AutoMapper; +using DigitalData.Core.Abstraction.Application.Repository; +using EnvelopeGenerator.Application.Envelopes.Queries; +using EnvelopeGenerator.Application.Receivers.Queries; +using MediatR; +using EnvelopeGenerator.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; +using EnvelopeGenerator.Application.Common.Query; +using EnvelopeGenerator.Application.Common.Extensions; + +namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries; + +/// +/// Represents a query for reading an envelope receiver including sensitive fields +/// (access code, phone number) that are excluded from the standard . +/// +/// +/// Returns a single matched by UUID and receiver signature. +/// Equivalent to the legacy ReadWithSecretByUuidSignatureAsync service method. +/// +public record ReadEnvelopeReceiverSecretQuery + : EnvelopeReceiverQueryBase, + IRequest; + +/// +/// Extension methods for dispatching via . +/// +public static class ReadEnvelopeReceiverSecretQueryExtensions +{ + /// + /// Sends a using the composite key (uuid::signature). + /// + /// The mediator instance. + /// Composite key in the format uuid::signature. + /// Cancellation token. + /// The matching , or null if not found. + public static Task ReadEnvelopeReceiverSecretAsync( + this IMediator mediator, + string key, + CancellationToken cancel = default) + => mediator.Send(new ReadEnvelopeReceiverSecretQuery { Key = key }, cancel); + + /// + /// Sends a using UUID and receiver signature. + /// + /// The mediator instance. + /// Envelope UUID. + /// Receiver signature. + /// Cancellation token. + /// The matching , or null if not found. + public static Task ReadEnvelopeReceiverSecretAsync( + this IMediator mediator, + string uuid, + string signature, + CancellationToken cancel = default) + { + var q = new ReadEnvelopeReceiverSecretQuery(); + q.Envelope.Uuid = uuid; + q.Receiver.Signature = signature; + return mediator.Send(q, cancel); + } + + /// + /// Handles and returns a + /// containing sensitive fields. + /// + public class ReadEnvelopeReceiverSecretQueryHandler + : IRequestHandler + { + private readonly IRepository _repo; + private readonly IRepository _rcvRepo; + private readonly IMapper _mapper; + + /// + /// Initializes a new instance of . + /// + /// Repository for . + /// Repository for . + /// AutoMapper instance. + public ReadEnvelopeReceiverSecretQueryHandler( + IRepository envelopeReceiver, + IRepository rcvRepo, + IMapper mapper) + { + _repo = envelopeReceiver; + _rcvRepo = rcvRepo; + _mapper = mapper; + } + + /// + /// Handles the query and returns the matching . + /// + /// The query containing filter criteria. + /// Cancellation token. + /// + /// The matched , or null if no record is found. + /// + public async Task Handle( + ReadEnvelopeReceiverSecretQuery request, + CancellationToken cancel) + { + var q = _repo.Query.Where(request, notnull: false); + + var envRcvs = await q + .Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements) + .Include(er => er.Envelope).ThenInclude(e => e!.Histories) + .Include(er => er.Envelope).ThenInclude(e => e!.User) + .Include(er => er.Receiver) + .ToListAsync(cancel); + + if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0) + { + var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel); + + foreach (var item in envRcvs) + item.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id); + } + + var envRcv = envRcvs.FirstOrDefault(); + if (envRcv is null) + return null; + + return _mapper.Map(envRcv); + } + } +} diff --git a/EnvelopeGenerator.DependencyInjection/DependencyInjection.cs b/EnvelopeGenerator.DependencyInjection/DependencyInjection.cs new file mode 100644 index 00000000..8bbbc853 --- /dev/null +++ b/EnvelopeGenerator.DependencyInjection/DependencyInjection.cs @@ -0,0 +1,145 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using EnvelopeGenerator.Application; +using EnvelopeGenerator.Application.Common.Interfaces.Services; +using EnvelopeGenerator.Application.Services; +using EnvelopeGenerator.Infrastructure; +using DigitalData.EmailProfilerDispatcher; +using DigitalData.UserManager.DependencyInjection; + +namespace EnvelopeGenerator.DependencyInjection; + +/// +/// Controls which optional services are registered by . +/// All flags default to true. Set a flag to false if the consuming project +/// already registers that service itself or simply does not need it. +/// +public sealed class EnvelopeGeneratorOptions +{ + /// Calls AddHttpContextAccessor(). Default: true. + public bool AddHttpContextAccessor { get; set; } = true; + + /// + /// Calls AddDistributedSqlServerCache() with the supplied . + /// Requires to be configured. Default: true. + /// + public bool AddDistributedSqlServerCache { get; set; } = true; + + /// + /// Options for the distributed SQL Server cache. + /// Required when is true. + /// + public SqlCacheOptions? SqlCacheOptions { get; set; } + + /// Calls AddDispatcher<TDbContext>(). Default: true. + public bool AddDispatcher { get; set; } = true; + + /// Calls AddMemoryCache(). Default: true. + public bool AddMemoryCache { get; set; } = true; + + /// Calls AddUserManager<TDbContext>(). Default: true. + public bool AddUserManager { get; set; } = true; +} + +/// Options for AddDistributedSqlServerCache. +public sealed class SqlCacheOptions +{ + /// SQL Server connection string. + public string ConnectionString { get; set; } = string.Empty; + + /// Schema name. Default: dbo. + public string SchemaName { get; set; } = "dbo"; + + /// Table name. Default: TBDD_CACHE. + public string TableName { get; set; } = "TBDD_CACHE"; +} + +/// +/// Extension methods for registering EnvelopeGenerator services into an . +/// Use as the single entry-point for projects that need both the +/// application layer (MediatR, AutoMapper, CRUD services, configuration sections) and the infrastructure +/// layer (repositories, DbContext, SQL executors). +/// For projects that do not need a database (e.g. lightweight API gateways or unit-test hosts), use +/// to register only the application layer. +/// +public static class DependencyInjection +{ + /// + /// Registers the full EnvelopeGenerator stack using as the DbContext type. + /// + public static IServiceCollection AddEnvelopeGenerator( + this IServiceCollection services, + IConfiguration configuration, + Action? infrastructureOptions = null, + Action? options = null) + { + var opt = new EnvelopeGeneratorOptions(); + options?.Invoke(opt); + +#pragma warning disable CS0618 + // Application layer: CRUD services, MediatR, AutoMapper, configuration sections. + services.AddEnvelopeGeneratorServices(configuration); + + // Infrastructure layer: repositories, DbContext, Dapper type maps, SQL executors. + services.AddEnvelopeGeneratorInfrastructureServices(cfg => + { + infrastructureOptions?.Invoke(cfg); + }); +#pragma warning restore CS0618 + + if (opt.AddHttpContextAccessor) + services.AddHttpContextAccessor(); + + if (opt.AddDistributedSqlServerCache && opt.SqlCacheOptions is { } cacheOpts) + services.AddDistributedSqlServerCache(o => + { + o.ConnectionString = cacheOpts.ConnectionString; + o.SchemaName = cacheOpts.SchemaName; + o.TableName = cacheOpts.TableName; + }); + + if (opt.AddDispatcher) + services.AddDispatcher(); + + if (opt.AddMemoryCache) + services.AddMemoryCache(); + +#pragma warning disable CS0618 + if (opt.AddUserManager) + services.AddUserManager(); +#pragma warning restore CS0618 + + return services; + } + + /// + /// Registers only the application layer services (MediatR handlers, AutoMapper profiles, + /// CRUD services, configuration sections) without any infrastructure / database dependencies. + /// + /// Service collection to register services into. + /// Application configuration used to bind application-level option sections. + /// The updated . +#pragma warning disable CS0618 + public static IServiceCollection AddEnvelopeGeneratorCore( + this IServiceCollection services, + IConfiguration configuration) + { + services.AddEnvelopeGeneratorServices(configuration); + return services; + } +#pragma warning restore CS0618 + + /// + /// Registers as the scoped + /// implementation. + /// + /// Service collection to register services into. + /// The updated . +#pragma warning disable CS0618 + public static IServiceCollection AddEnvelopeMailService(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +#pragma warning restore CS0618 +} diff --git a/EnvelopeGenerator.DependencyInjection/EnvelopeGenerator.DependencyInjection.csproj b/EnvelopeGenerator.DependencyInjection/EnvelopeGenerator.DependencyInjection.csproj new file mode 100644 index 00000000..a504ed13 --- /dev/null +++ b/EnvelopeGenerator.DependencyInjection/EnvelopeGenerator.DependencyInjection.csproj @@ -0,0 +1,176 @@ + + + + net7.0;net8.0;net9.0 + enable + enable + + + EnvelopeGenerator + Digital Data GmbH + Digital Data GmbH + EnvelopeGenerator + + Envelope Generator ist eine Bibliothek zur Verwaltung und Verarbeitung digitaler Umschläge (Envelopes). + Dieses Paket enthält die Dependency-Injection-Erweiterungsmethoden und bündelt die Application- + sowie Infrastructure-Schicht in einer einzigen NuGet-Referenz. + + Copyright 2024 Digital Data GmbH + http://git.dd:3000/AppStd/EnvelopeGenerator.git + digital data envelope generator di dependency injection + false + 1.2.0.3 + 1.2.0.3 + 1.2.0.3 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + + + all + + + all + + + + + + + <_DepDlls Include=" ..\EnvelopeGenerator.Application\bin\$(Configuration)\%(ProjectReference.TargetFramework)\EnvelopeGenerator.Application.dll; ..\EnvelopeGenerator.Domain\bin\$(Configuration)\%(ProjectReference.TargetFramework)\EnvelopeGenerator.Domain.dll; ..\EnvelopeGenerator.Infrastructure\bin\$(Configuration)\%(ProjectReference.TargetFramework)\EnvelopeGenerator.Infrastructure.dll" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs index e8068bd2..4fc4f10c 100644 --- a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs @@ -438,4 +438,10 @@ public class EnvelopeController : ViewControllerBase return this.ViewDocumentNotFound(); } } + + [HttpGet("EnvelopeReceiverWithSecretByMediatr")] + public async Task EnvelopeReceiverWithSecretByMediatr([FromQuery] ReadEnvelopeReceiverSecretQuery q, CancellationToken cancel) + { + return await _mediator.Send(q, cancel) is EnvelopeReceiverSecretDto dto ? Ok(dto) : NotFound(); + } } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj index 6c0e5d59..d0071918 100644 --- a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj +++ b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj @@ -2175,6 +2175,7 @@ + diff --git a/EnvelopeGenerator.Web/Program.cs b/EnvelopeGenerator.Web/Program.cs index 4e158971..f25a9888 100644 --- a/EnvelopeGenerator.Web/Program.cs +++ b/EnvelopeGenerator.Web/Program.cs @@ -1,4 +1,3 @@ -using EnvelopeGenerator.Application.Services; using Microsoft.EntityFrameworkCore; using NLog; using Quartz; @@ -9,14 +8,12 @@ using EnvelopeGenerator.Web.Models; using System.Text.Encodings.Web; using Ganss.Xss; using Microsoft.Extensions.Options; -using EnvelopeGenerator.Application; using DigitalData.EmailProfilerDispatcher; using EnvelopeGenerator.Infrastructure; using EnvelopeGenerator.Web.Sanitizers; using EnvelopeGenerator.Web.Models.Annotation; -using DigitalData.UserManager.DependencyInjection; using EnvelopeGenerator.Web.Middleware; -using EnvelopeGenerator.Application.Common.Interfaces.Services; +using EnvelopeGenerator.DependencyInjection; using EnvelopeGenerator.Web; var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); @@ -57,8 +54,6 @@ try }); }); - builder.Services.AddHttpContextAccessor(); - builder.ConfigureBySection(); // Add controllers and razor views @@ -95,17 +90,9 @@ try var connStr = config.GetConnectionString(cnnStrName) ?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration."); - builder.Services.AddDistributedSqlServerCache(options => - { - options.ConnectionString = connStr; - options.SchemaName = "dbo"; - options.TableName = "TBDD_CACHE"; - }); - // Add envelope generator services -#pragma warning disable CS0618 // Type or member is obsolete - builder.Services.AddEnvelopeGeneratorInfrastructureServices( - opt => + builder.Services.AddEnvelopeGenerator(config, + infrastructureOptions: opt => { opt.AddDbTriggerParams(config); opt.AddDbContext((provider, options) => @@ -116,12 +103,16 @@ try .EnableSensitiveDataLogging() .EnableDetailedErrors(); }); + }, + options: opt => + { + opt.SqlCacheOptions = new() + { + ConnectionString = connStr, + SchemaName = "dbo", + TableName = "TBDD_CACHE" + }; }); -#pragma warning restore CS0618 // Type or member is obsolete - -#pragma warning disable CS0618 // Type or member is obsolete - builder.Services.AddEnvelopeGeneratorServices(config); -#pragma warning restore CS0618 // Type or member is obsolete builder.Services.Configure(options => { @@ -169,22 +160,12 @@ try builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); // Register mail services -#pragma warning disable CS0618 // Type or member is obsolete - builder.Services.AddScoped(); -#pragma warning restore CS0618 // Type or member is obsolete - - builder.Services.AddDispatcher(); - - builder.Services.AddMemoryCache(); + builder.Services.AddEnvelopeMailService(); builder.ConfigureBySection(); builder.ConfigureBySection(); -#pragma warning disable CS0618 // Type or member is obsolete - builder.Services.AddUserManager(); -#pragma warning restore CS0618 // Type or member is obsolete - var app = builder.Build(); app.UseMiddleware(); diff --git a/EnvelopeGenerator.Web/wwwroot/js/util.min.js b/EnvelopeGenerator.Web/wwwroot/js/util.min.js index 3f35065e..61eb8277 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/util.min.js +++ b/EnvelopeGenerator.Web/wwwroot/js/util.min.js @@ -1 +1 @@ -function detailedCurrentDate(){return new Intl.DateTimeFormat(getCurrentCulture(),{day:"2-digit",month:"2-digit",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"shortOffset"}).format()}function findNearest(e,t,o,n){const r=n.reduce(((n,r)=>{const i=Math.sqrt((t(e)-t(r))**2+(o(e)-o(r))**2);return i("undefined"!=typeof localized&&localized.culture)?localized.culture:navigator.language||"en-US";const B64ToBuff=e=>new Uint8Array(Array.from(atob(e),e=>e.charCodeAt(0))).buffer;const getLocaleDateString=e=>new Date().toLocaleDateString(getCurrentCulture()); \ No newline at end of file +function detailedCurrentDate(){return new Intl.DateTimeFormat(getCurrentCulture(),{day:"2-digit",month:"2-digit",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"shortOffset"}).format()}function findNearest(n,t,i,r){const u=r=>Math.sqrt((t(n)-t(r))**2+(i(n)-i(r))**2);return r.reduce((n,t)=>{const i=u(t);return itypeof localized!="undefined"&&localized.culture?localized.culture:navigator.language||"en-US",B64ToBuff=n=>new Uint8Array(Array.from(atob(n),n=>n.charCodeAt(0))).buffer,getLocaleDateString=()=>(new Date).toLocaleDateString(getCurrentCulture()); \ No newline at end of file diff --git a/EnvelopeGenerator.sln b/EnvelopeGenerator.sln index a99a93c8..35d850e3 100644 --- a/EnvelopeGenerator.sln +++ b/EnvelopeGenerator.sln @@ -87,10 +87,6 @@ Global {EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU - {FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -110,7 +106,6 @@ Global {211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB} {224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146} {EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} - {FB2D306B-1042-4A70-31ED-F991A1599371} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}