diff --git a/EnvelopeGenerator.Application/Contracts/SQLExecutor/IDocumentExecutor.cs b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IDocumentExecutor.cs new file mode 100644 index 00000000..e3163f5e --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IDocumentExecutor.cs @@ -0,0 +1,18 @@ +using EnvelopeGenerator.Domain.Entities; + +namespace EnvelopeGenerator.Application.Contracts.SQLExecutor; + +/// +/// +/// +public interface IDocumentExecutor +{ + /// + /// + /// + /// + /// + /// + /// + Task CreateDocumentAsync(string base64, string envelope_uuid, CancellationToken cancellation = default); +} diff --git a/EnvelopeGenerator.Application/Contracts/SQLExecutor/IEnvelopeExecutor.cs b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IEnvelopeExecutor.cs new file mode 100644 index 00000000..25c5d29c --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IEnvelopeExecutor.cs @@ -0,0 +1,21 @@ +using Dapper; +using EnvelopeGenerator.Domain.Entities; + +namespace EnvelopeGenerator.Application.Contracts.SQLExecutor; + +/// +/// +/// +public interface IEnvelopeExecutor : ISQLExecutor +{ + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task CreateEnvelopeAsync(int userId, string title = "", string message = "", bool tfaEnabled = false, CancellationToken cancellation = default); +} diff --git a/EnvelopeGenerator.Application/Contracts/SQLExecutor/IEnvelopeReceiverExecutor.cs b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IEnvelopeReceiverExecutor.cs new file mode 100644 index 00000000..71cd8faa --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IEnvelopeReceiverExecutor.cs @@ -0,0 +1,20 @@ +using EnvelopeGenerator.Domain.Entities; + +namespace EnvelopeGenerator.Application.Contracts.SQLExecutor; + +/// +/// +/// +public interface IEnvelopeReceiverExecutor +{ + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AddEnvelopeReceiverAsync(string envelope_uuid, string emailAddress, string? salutation = null, string? phone = null, CancellationToken cancellation = default); +} diff --git a/EnvelopeGenerator.Application/Contracts/SQLExecutor/IQuery.cs b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IQuery.cs new file mode 100644 index 00000000..c0be3674 --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/SQLExecutor/IQuery.cs @@ -0,0 +1,70 @@ +namespace EnvelopeGenerator.Application.Contracts.SQLExecutor; + +/// +/// Provides methods for executing common queries on a given entity type. +/// This interface abstracts away the direct usage of ORM libraries (such as Entity Framework) for querying data +/// and provides asynchronous and synchronous operations for querying a collection or single entity. +/// +/// The type of the entity being queried. +public interface IQuery +{ + /// + /// Asynchronously retrieves the first entity or a default value if no entity is found. + /// + /// A task that represents the asynchronous operation. The task result contains the entity or a default value. + public Task FirstOrDefaultAsync(); + + /// + /// Asynchronously retrieves a single entity or a default value if no entity is found. + /// + /// A task that represents the asynchronous operation. The task result contains the entity or a default value. + public Task SingleOrDefaultAsync(); + + /// + /// Asynchronously retrieves a list of entities. + /// + /// A task that represents the asynchronous operation. The task result contains the list of entities. + public Task> ToListAsync(); + + /// + /// Asynchronously retrieves the first entity. Throws an exception if no entity is found. + /// + /// A task that represents the asynchronous operation. The task result contains the first entity. + public Task FirstAsync(); + + /// + /// Asynchronously retrieves a single entity. Throws an exception if no entity is found. + /// + /// A task that represents the asynchronous operation. The task result contains the single entity. + public Task SingleAsync(); + + /// + /// Synchronously retrieves the first entity or a default value if no entity is found. + /// + /// The first entity or a default value. + public TEntity? FirstOrDefault(); + + /// + /// Synchronously retrieves a single entity or a default value if no entity is found. + /// + /// The single entity or a default value. + public TEntity? SingleOrDefault(); + + /// + /// Synchronously retrieves a list of entities. + /// + /// The list of entities. + public IEnumerable ToList(); + + /// + /// Synchronously retrieves the first entity. Throws an exception if no entity is found. + /// + /// The first entity. + public TEntity First(); + + /// + /// Synchronously retrieves a single entity. Throws an exception if no entity is found. + /// + /// The single entity. + public TEntity Single(); +} diff --git a/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQL.cs b/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQL.cs new file mode 100644 index 00000000..00c0cd8d --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQL.cs @@ -0,0 +1,20 @@ +namespace EnvelopeGenerator.Application.Contracts.SQLExecutor; + +/// +/// Represents a raw SQL query contract. +/// +public interface ISQL +{ + /// + /// Gets the raw SQL query string. + /// + string Raw { get; } +} + +/// +/// Represents a typed SQL query contract for a specific entity. +/// +/// The type of the entity associated with the SQL query. +public interface ISQL : ISQL +{ +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQLExecutor.cs b/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQLExecutor.cs new file mode 100644 index 00000000..0c6ea474 --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQLExecutor.cs @@ -0,0 +1,27 @@ +namespace EnvelopeGenerator.Application.Contracts.SQLExecutor; + +/// +/// Defines methods for executing raw SQL queries or custom SQL query classes and returning query executors for further operations. +/// Provides abstraction for raw SQL execution as well as mapping the results to objects. +/// +/// The entity type to which the SQL query results will be mapped. +public interface ISQLExecutor: ISQLExecutor +{ + /// + /// Executes a raw SQL query and returns an for further querying operations on the result. + /// + /// The raw SQL query to execute. + /// Optional cancellation token for the operation. + /// Optional parameters for the SQL query. + /// An instance for further query operations on the result. + IQuery Execute(string sql, CancellationToken cancellation = default, params object[] parameters); + + /// + /// Executes a custom SQL query defined by a class that implements and returns an for further querying operations on the result. + /// + /// The type of the custom SQL query class implementing . + /// Optional cancellation token for the operation. + /// Optional parameters for the SQL query. + /// An instance for further query operations on the result. + IQuery Execute(CancellationToken cancellation = default, params object[] parameters) where TSQL : ISQL; +} diff --git a/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQLExecutorBaseEntity.cs b/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQLExecutorBaseEntity.cs new file mode 100644 index 00000000..afe03e9d --- /dev/null +++ b/EnvelopeGenerator.Application/Contracts/SQLExecutor/ISQLExecutorBaseEntity.cs @@ -0,0 +1,29 @@ +using Dapper; + +namespace EnvelopeGenerator.Application.Contracts.SQLExecutor; + +/// +/// +/// +public interface ISQLExecutor +{ + /// + /// Executes a raw SQL query and returns an for further querying operations on the result. + /// + /// The entity type to which the SQL query results will be mapped. + /// The raw SQL query to execute. + /// Parameters for the SQL query. + /// Optional cancellation token for the operation. + /// An instance for further query operations on the result. + Task> Execute(string sql, DynamicParameters parameters, CancellationToken cancellation = default); + + /// + /// Executes a custom SQL query defined by a class that implements and returns an for further querying operations on the result. + /// + /// The entity type to which the SQL query results will be mapped. + /// The type of the custom SQL query class implementing . + /// Parameters for the SQL query. + /// Optional cancellation token for the operation. + /// An instance for further query operations on the result. + Task> Execute(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/Services/IEnvelopeReceiverService.cs b/EnvelopeGenerator.Application/Contracts/Services/IEnvelopeReceiverService.cs index cc384954..98a05635 100644 --- a/EnvelopeGenerator.Application/Contracts/Services/IEnvelopeReceiverService.cs +++ b/EnvelopeGenerator.Application/Contracts/Services/IEnvelopeReceiverService.cs @@ -3,6 +3,8 @@ using DigitalData.Core.Abstractions.Application; using DigitalData.Core.DTO; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; using EnvelopeGenerator.Application.DTOs.Messaging; +using EnvelopeGenerator.Application.Envelopes; +using EnvelopeGenerator.Application.Receivers.Queries.Read; using EnvelopeGenerator.Domain.Entities; namespace EnvelopeGenerator.Application.Contracts.Services; @@ -31,7 +33,7 @@ public interface IEnvelopeReceiverService : IBasicCRUDService> IsExisting(string envelopeReceiverId); - Task>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses); + Task>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, EnvelopeQuery? envelopeQuery = null, ReadReceiverQuery? receiverQuery = null, params int[] ignore_statuses); Task> ReadLastUsedReceiverNameByMail(string mail); diff --git a/EnvelopeGenerator.Application/DTOs/EmailTemplateDto.cs b/EnvelopeGenerator.Application/DTOs/EmailTemplateDto.cs index e22a4844..d9b82487 100644 --- a/EnvelopeGenerator.Application/DTOs/EmailTemplateDto.cs +++ b/EnvelopeGenerator.Application/DTOs/EmailTemplateDto.cs @@ -3,10 +3,30 @@ using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.DTOs { + /// + /// + /// [ApiExplorerSettings(IgnoreApi = true)] - public record EmailTemplateDto( - int Id, - string Name, - string Body, - string Subject) : IUnique; + public record EmailTemplateDto : IUnique + { + /// + /// + /// + public int Id{ get; init; } + + /// + /// + /// + public required string Name { get; init; } + + /// + /// + /// + public required string Body { get; set; } + + /// + /// + /// + public required string Subject { get; set; } + }; } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/EnvelopeDto.cs b/EnvelopeGenerator.Application/DTOs/EnvelopeDto.cs index 6a038d83..c748b038 100644 --- a/EnvelopeGenerator.Application/DTOs/EnvelopeDto.cs +++ b/EnvelopeGenerator.Application/DTOs/EnvelopeDto.cs @@ -66,6 +66,9 @@ namespace EnvelopeGenerator.Application.DTOs public string? StatusTranslated { get; set; } public string? ContractTypeTranslated { get; set; } - public IEnumerable? Documents { get; set; } + + public byte[]? DocResult { get; init; } + + public IEnumerable? Documents { get; set; } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs index 41e380c8..4f2912a6 100644 --- a/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs +++ b/EnvelopeGenerator.Application/DTOs/Receiver/ReceiverReadDto.cs @@ -1,6 +1,5 @@ using DigitalData.Core.Abstractions; using DigitalData.Core.DTO; -using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver; using Microsoft.AspNetCore.Mvc; using System.Text.Json.Serialization; diff --git a/EnvelopeGenerator.Application/DependencyInjection.cs b/EnvelopeGenerator.Application/DependencyInjection.cs index 0d96155c..4e3904d1 100644 --- a/EnvelopeGenerator.Application/DependencyInjection.cs +++ b/EnvelopeGenerator.Application/DependencyInjection.cs @@ -7,6 +7,7 @@ using DigitalData.Core.Client; using QRCoder; using EnvelopeGenerator.Application.Contracts.Services; using System.Reflection; +using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; namespace EnvelopeGenerator.Application; @@ -58,6 +59,7 @@ public static class DependencyInjection services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); + cfg.RegisterServicesFromAssembly(typeof(CreateEnvelopeReceiverCommandHandler).Assembly); }); return services; diff --git a/EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEnvelopeTemplateCommand.cs b/EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEmailTemplateCommand.cs similarity index 64% rename from EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEnvelopeTemplateCommand.cs rename to EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEmailTemplateCommand.cs index 79ce1295..1c8fb830 100644 --- a/EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEnvelopeTemplateCommand.cs +++ b/EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEmailTemplateCommand.cs @@ -1,4 +1,5 @@ using EnvelopeGenerator.Common; +using MediatR; namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset; @@ -6,8 +7,7 @@ namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset; /// Ein Befehl zum Zurücksetzen einer E-Mail-Vorlage auf die Standardwerte. /// Erbt von und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage. /// -/// Die optionale ID der E-Mail-Vorlage, die zurückgesetzt werden soll. -/// Der Typ der E-Mail-Vorlage, z. B. (optional). Beispiele: +/// Beispiele: /// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments. /// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments. /// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments. @@ -19,4 +19,23 @@ namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset; /// 8 - DocumentRejected_REC (Für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird. /// 9 - DocumentRejected_REC_2 (Für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird. /// -public record ResetEnvelopeTemplateCommand(int? Id, Constants.EmailTemplateType? Type) : EmailTemplateQuery(Id, Type); +public record ResetEmailTemplateCommand : EmailTemplateQuery, IRequest +{ + /// + /// + /// + /// + public ResetEmailTemplateCommand(EmailTemplateQuery? orginal = null) : base(orginal ?? new()) + { + + } + + /// + /// + /// + /// Die optionale ID der E-Mail-Vorlage, die zurückgesetzt werden soll. + /// Der Typ der E-Mail-Vorlage, z. B. (optional). + public ResetEmailTemplateCommand(int? Id = null, Constants.EmailTemplateType? Type = null) : base(Id, Type) + { + } +}; diff --git a/EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEmailTemplateCommandHandler.cs b/EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEmailTemplateCommandHandler.cs new file mode 100644 index 00000000..ab5300cf --- /dev/null +++ b/EnvelopeGenerator.Application/EmailTemplates/Commands/Reset/ResetEmailTemplateCommandHandler.cs @@ -0,0 +1,114 @@ +using DigitalData.Core.Abstractions.Infrastructure; +using EnvelopeGenerator.Application.DTOs; +using EnvelopeGenerator.Application.EmailTemplates.Queries.Read; +using EnvelopeGenerator.Domain.Entities; +using MediatR; + +namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset; + +/// +/// +/// +public class ResetEmailTemplateCommandHandler : IRequestHandler +{ + private readonly IRepository _repository; + + /// + /// + /// + /// + public ResetEmailTemplateCommandHandler(IRepository repository) + { + _repository = repository; + } + + /// + /// + /// + /// + /// + /// + public async Task Handle(ResetEmailTemplateCommand request, CancellationToken cancel) + { + var temps = request.Id is not null + ? await _repository.ReadAllAsync(t => t.Id == request.Id, cancel) + : request.Type is not null + ? await _repository.ReadAllAsync(t => t.Name == request.Type.ToString(), cancel) + : await _repository.ReadAllAsync(ct: cancel); + + foreach (var temp in temps) + { + var def = Defaults.Where(t => t.Name == temp.Name).FirstOrDefault(); + if(def is not null) + await _repository.UpdateAsync(def, t => t.Id == temp.Id, cancel); + } + } + + /// + /// + /// + public static readonly IEnumerable Defaults = new List() + { + new(){ + Id = 1, + Name = "DocumentReceived", + Body = "Guten Tag [NAME_RECEIVER],
\r\n
\r\n[NAME_SENDER] hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet.
\r\n
\r\nÜber den folgenden Link können Sie das Dokument einsehen und elektronisch unterschreiben: [LINK_TO_DOCUMENT_TEXT]
\r\n
\r\n[MESSAGE]
\r\n
\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "Dokument erhalten: '[DOCUMENT_TITLE]'" + }, + new(){ + Id = 2, + Name = "DocumentDeleted", + Body = "Guten Tag [NAME_RECEIVER],
\r\n
\r\n[NAME_SENDER] hat den Umschlag '[DOCUMENT_TITLE]' gelöscht/zurückgezogen.

\rBegründung:
[REASON]

\r\n
\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "Umschlag zurückgezogen: '[DOCUMENT_TITLE]'" + }, + new(){ + Id = 3, + Name = "DocumentSigned", + Body = "Guten Tag [NAME_RECEIVER],
\r\n
\r\nhiermit bestätigen wir Ihnen die erfolgreiche Signatur für den Vorgang '[DOCUMENT_TITLE]'.
\r\nWenn alle Vertragspartner unterzeichnet haben, erhalten Sie ebenfalls per email ein unterschriebenes Exemplar mit dem Signierungszertifikat!\r\n
\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "Dokument unterschrieben: '[DOCUMENT_TITLE]'" + }, + new(){ + Id = 4, + Name = "DocumentCompleted", + Body = "Guten Tag [NAME_RECEIVER],
\r\n
\r\nDer Signaturvorgang '[DOCUMENT_TITLE]' wurde erfolgreich abgeschlossen.
\r\n
\r\nSie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.
\r\n
\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "Umschlag abgeschlossen: '[DOCUMENT_TITLE]'" + }, + new(){ + Id = 5, + Name = "DocumentAccessCodeReceived", + Body = "Guten Tag [NAME_RECEIVER],
\r\n
\r\n[NAME_SENDER] hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet.
\r\n
\r\nVerwenden Sie den folgenden Zugriffscode, um das Dokument einzusehen:
\r\n
\r\n[DOCUMENT_ACCESS_CODE]
\r\n
\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "Zugriffscode für Dokument erhalten: '[DOCUMENT_TITLE]'" + }, + new(){ + Id = 6, + Name = "DocumentRejected_ADM", + Body = "Guten Tag [NAME_SENDER],

[NAME_RECEIVER] hat den Umschlag '[DOCUMENT_TITLE]' mit folgendem Grund abgelehnt:

\r\n[REASON] \r\n

Der Umschlag wurde auf den Status Rejected gesetzt.

\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "'[DOCUMENT_TITLE]' - Unterzeichnungsvorgang zurückgezogen" + }, + new(){ + Id = 9, + Name = "DocumentRejected_REC", + Body = "Guten Tag [NAME_RECEIVER],\r\n

Hiermit bestätigen wir Ihnen die Ablehnung des Unterzeichnungsvorganges '[DOCUMENT_TITLE]'!

Der Vertragsinhaber [NAME_SENDER] wurde über die Ablehnung informiert.

\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "'[DOCUMENT_TITLE]' - Bestätigung Ablehnung" + }, + new(){ + Id = 10, + Name = "DocumentRejected_REC_2", + Body = "Guten Tag [NAME_RECEIVER],\r\n

Der Unterzeichnungsvorganges '[DOCUMENT_TITLE]' wurde durch einen anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.

Der Vertragsinhaber [NAME_SENDER] wird sich bei Bedarf mit Ihnen in Verbindung setzen.

\r\nMit freundlichen Grüßen
\r\n
\r\n[NAME_PORTAL]", + Subject = "'[DOCUMENT_TITLE]' - Unterzeichnungsvorgang abgelehnt." + }, + new(){ + Id = 11, + Name = "DocumentShared", + Body = "Guten Tag,

[NAME_RECEIVER] hat Ihnen ein Dokument zum Ansehen gesendet.

Über den folgenden Link können Sie das Dokument einsehen: [LINK_TO_DOCUMENT_TEXT]


Mit freundlichen Grüßen

[NAME_PORTAL]", + Subject = "Dokument geteilt: '[DOCUMENT_TITLE]'" + }, + new(){ + Id = 12, + Name = "TotpSecret", + Body = "Guten Tag,

Sie können auf Ihren Zwei-Faktor-Authentifizierungscode zugreifen, indem Sie den unten stehenden QR-Code mit einer beliebigen Authentifizierungs-App auf Ihrem Telefon scannen (Google Authenticator, Microsoft Authenticator usw.). Dieser Code ist bis zum [TFA_EXPIRATION] gültig.



\r\n
Mit freundlichen Grüßen

[NAME_PORTAL]", + Subject = "2-Faktor-Verifizierung QR-Code" + } + }; + +} diff --git a/EnvelopeGenerator.Application/EmailTemplates/Commands/Update/UpdateEmailTemplateCommand.cs b/EnvelopeGenerator.Application/EmailTemplates/Commands/Update/UpdateEmailTemplateCommand.cs index 63d2a7fd..1f3d7525 100644 --- a/EnvelopeGenerator.Application/EmailTemplates/Commands/Update/UpdateEmailTemplateCommand.cs +++ b/EnvelopeGenerator.Application/EmailTemplates/Commands/Update/UpdateEmailTemplateCommand.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using MediatR; +using System.Text.Json.Serialization; namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Update; @@ -12,11 +13,17 @@ namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Update; /// /// (Optional) Der neue Betreff der E-Mail. Wenn null, bleibt der vorhandene Betreff unverändert. /// -public record UpdateEmailTemplateCommand(string? Body = null, string? Subject = null) +public record UpdateEmailTemplateCommand(string? Body = null, string? Subject = null) : IRequest { /// /// Die Abfrage, die die E-Mail-Vorlage darstellt, die aktualisiert werden soll. /// [JsonIgnore] public EmailTemplateQuery? EmailTemplateQuery { get; set; } + + ///

+ /// + /// + [JsonIgnore] + public DateTime ChangedWhen { get; init; } = DateTime.Now; } diff --git a/EnvelopeGenerator.Application/EmailTemplates/Commands/Update/UpdateEmailTemplateCommandHandler.cs b/EnvelopeGenerator.Application/EmailTemplates/Commands/Update/UpdateEmailTemplateCommandHandler.cs new file mode 100644 index 00000000..4912e2e6 --- /dev/null +++ b/EnvelopeGenerator.Application/EmailTemplates/Commands/Update/UpdateEmailTemplateCommandHandler.cs @@ -0,0 +1,63 @@ +using DigitalData.Core.Abstractions.Infrastructure; +using EnvelopeGenerator.Application.DTOs; +using EnvelopeGenerator.Application.Exceptions; +using EnvelopeGenerator.Domain.Entities; +using MediatR; + +namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Update; + +/// +/// +/// +public class UpdateEmailTemplateCommandHandler : IRequestHandler +{ + private readonly IRepository _repository; + + /// + /// + /// + /// + public UpdateEmailTemplateCommandHandler(IRepository repository) + { + _repository = repository; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public async Task Handle(UpdateEmailTemplateCommand request, CancellationToken cancel) + { + EmailTemplateDto? temp; + + if (request.EmailTemplateQuery?.Id is int id) + { + temp = await _repository.ReadOrDefaultAsync(t => t.Id == id, single: false, cancel); + } + else if (request!.EmailTemplateQuery!.Type is Common.Constants.EmailTemplateType type) + { + temp = await _repository.ReadOrDefaultAsync(t => t.Name == type.ToString(), single: false, cancel); + } + else + { + throw new InvalidOperationException("Both id and type is null. Id: " + request.EmailTemplateQuery.Id +". Type: " + request.EmailTemplateQuery.Type.ToString()); + } + + if (temp == null) + { + throw new NotFoundException(); + } + + if (request.Body is not null) + temp.Body = request.Body; + + if (request.Subject is not null) + temp.Subject = request.Subject; + + await _repository.UpdateAsync(temp, t => t.Id == temp.Id, cancel); + } +} diff --git a/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateMappingProfile.cs b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateMappingProfile.cs new file mode 100644 index 00000000..ccdb298d --- /dev/null +++ b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateMappingProfile.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using EnvelopeGenerator.Domain.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read; + +/// +/// +/// +public class ReadEmailTemplateMappingProfile : Profile +{ + /// + /// + /// + public ReadEmailTemplateMappingProfile() + { + CreateMap(); + } +} diff --git a/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateQuery.cs b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateQuery.cs index 0c82bab2..b3108a30 100644 --- a/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateQuery.cs +++ b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateQuery.cs @@ -1,10 +1,12 @@ -namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read; +using MediatR; + +namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read; /// /// Stellt eine Abfrage dar, um eine E-Mail-Vorlage zu lesen. /// Diese Klasse erbt von . /// -public record ReadEmailTemplateQuery : EmailTemplateQuery +public record ReadEmailTemplateQuery : EmailTemplateQuery, IRequest { } diff --git a/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateQueryHandler.cs b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateQueryHandler.cs new file mode 100644 index 00000000..6059ec33 --- /dev/null +++ b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateQueryHandler.cs @@ -0,0 +1,52 @@ +using AutoMapper; +using EnvelopeGenerator.Application.Contracts.Repositories; +using EnvelopeGenerator.Application.DTOs; +using EnvelopeGenerator.Common; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read; + +/// +/// +/// +public class ReadEmailTemplateQueryHandler : IRequestHandler +{ + private readonly IMapper _mapper; + + private readonly IEmailTemplateRepository _repository; + + /// + /// Initialisiert eine neue Instanz der -Klasse. + /// + /// + /// + /// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird. + /// + public ReadEmailTemplateQueryHandler(IMapper mapper, IEmailTemplateRepository repository) + { + _mapper = mapper; + _repository = repository; + } + + /// + /// + /// + /// + /// + /// + /// + public async Task Handle(ReadEmailTemplateQuery request, CancellationToken cancellationToken) + { + var temp = request.Id is int id + ? await _repository.ReadByIdAsync(id) + : request.Type is Constants.EmailTemplateType type + ? await _repository.ReadByNameAsync(type) + : throw new InvalidOperationException("Either a valid integer ID or a valid EmailTemplateType must be provided in the request."); + + var res = _mapper.Map(temp); + + return res; + } +} diff --git a/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateResponse.cs b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateResponse.cs index 3234c86a..df68fa94 100644 --- a/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateResponse.cs +++ b/EnvelopeGenerator.Application/EmailTemplates/Queries/Read/ReadEmailTemplateResponse.cs @@ -3,18 +3,35 @@ /// /// Stellt die Antwort für eine Abfrage von E-Mail-Vorlagen bereit. /// -/// Die eindeutige Kennung der E-Mail-Vorlage. -/// Der Typ der E-Mail-Vorlage. -/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde. -/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein. -/// Der Betreff der E-Mail-Vorlage. Kann null sein. -/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein. -public record ReadEmailTemplateResponse( - int Id, - int Type, - DateTime AddedWhen, - string? Body = null, - string? Subject = null, - DateTime? ChangedWhen = null) +public class ReadEmailTemplateResponse { + /// + /// Die eindeutige Kennung der E-Mail-Vorlage. + /// + public int Id { get; set; } + + /// + /// Name des Typs + /// + public required string Name { get; set; } + + /// + /// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde. + /// + public DateTime AddedWhen { get; set; } + + /// + /// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein. + /// + public string? Body { get; set; } + + /// + /// Der Betreff der E-Mail-Vorlage. Kann null sein. + /// + public string? Subject { get; set; } + + /// + /// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein. + /// + public DateTime? ChangedWhen { get; set; } } diff --git a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj index 73ba45fe..e285a702 100644 --- a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj +++ b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj @@ -13,6 +13,7 @@ + @@ -20,6 +21,7 @@ + diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverCommand.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverCommand.cs index 7000950d..fb82abaf 100644 --- a/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverCommand.cs +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverCommand.cs @@ -1,4 +1,5 @@ -using MediatR; +using EnvelopeGenerator.Application.Envelopes.Commands; +using MediatR; using System.ComponentModel.DataAnnotations; namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; @@ -17,43 +18,4 @@ public record CreateEnvelopeReceiverCommand( [Required] DocumentCreateCommand Document, [Required] IEnumerable Receivers, bool TFAEnabled = false - ) : IRequest; - -#region DTOs -/// -/// Signaturposition auf einem Dokument. -/// -/// X-Position -/// Y-Position -/// Seite, auf der sie sich befindet -public record Signature([Required] int X, [Required] int Y, [Required] int Page); - -/// -/// DTO für Empfänger, die erstellt oder abgerufen werden sollen. -/// Wenn nicht, wird sie erstellt und mit einer Signatur versehen. -/// -/// Unterschriften auf Dokumenten. -/// Der Name, mit dem der Empfänger angesprochen werden soll. Bei Null oder keinem Wert wird der zuletzt verwendete Name verwendet. -/// Sollte mit Vorwahl geschrieben werden -public record ReceiverGetOrCreateCommand([Required] IEnumerable Signatures, string? Salution = null, string? PhoneNumber = null) -{ - private string _emailAddress = string.Empty; - - /// - /// E-Mail-Adresse des Empfängers. - /// - [Required] - public required string EmailAddress { get => _emailAddress.ToLower(); init => _emailAddress = _emailAddress.ToLower(); } -}; - -/// -/// DTO zum Erstellen eines Dokuments. -/// -/// -/// Die Dokumentdaten im Byte-Array-Format. Wird verwendet, wenn das Dokument als Roh-Binärdaten bereitgestellt wird. -/// -/// -/// Die Dokumentdaten im Base64-String-Format. Wird verwendet, wenn das Dokument als Base64-codierter String bereitgestellt wird. -/// -public record DocumentCreateCommand(byte[]? DataAsByte = null, string? DataAsBase64 = null); -#endregion \ No newline at end of file + ) : CreateEnvelopeCommand(Title, Message, TFAEnabled), IRequest; \ No newline at end of file diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverCommandHandler.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverCommandHandler.cs new file mode 100644 index 00000000..3ab6fda1 --- /dev/null +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverCommandHandler.cs @@ -0,0 +1,67 @@ +using AutoMapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Application.DTOs.Receiver; +using EnvelopeGenerator.Domain.Entities; +using MediatR; + +namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; + +/// +/// Handles the creation of an envelope along with its associated document and recipients. +/// This command processes the envelope data, including title, message, document content, +/// recipient list, and optional two-factor authentication settings. +/// +public class CreateEnvelopeReceiverCommandHandler : IRequestHandler +{ + private readonly IMapper _mapper; + + private readonly IEnvelopeExecutor _envelopeExecutor; + + private readonly IEnvelopeReceiverExecutor _erExecutor; + + /// + /// + /// + /// + /// + /// + public CreateEnvelopeReceiverCommandHandler(IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor) + { + _mapper = mapper; + _envelopeExecutor = envelopeExecutor; + _erExecutor = erExecutor; + } + + /// + /// Handles the execution of the . + /// Responsible for validating input data, creating or retrieving recipients, associating signatures, + /// and storing the envelope and document details. + /// + /// The command containing all necessary information to create an envelope. + /// Token to observe while waiting for the task to complete. + /// A task representing the asynchronous operation. + public async Task Handle(CreateEnvelopeReceiverCommand request, CancellationToken cancel) + { + int userId = request.UserId ?? throw new InvalidOperationException("UserId cannot be null when creating an envelope."); + + var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancel); + + List sentRecipients = new(); + List unsentRecipients = new(); + + foreach (var receiver in request.Receivers) + { + var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel); + + if (envelopeReceiver is null) + unsentRecipients.Add(receiver); + else + sentRecipients.Add(envelopeReceiver); + } + + var res = _mapper.Map(envelope); + res.UnsentReceivers = unsentRecipients; + res.SentReceiver = _mapper.Map>(sentRecipients); + return res; + } +} diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverDtos.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverDtos.cs new file mode 100644 index 00000000..dc8072e0 --- /dev/null +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverDtos.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; + +#region DTOs +/// +/// Signaturposition auf einem Dokument. +/// +/// X-Position +/// Y-Position +/// Seite, auf der sie sich befindet +public record Signature([Required] double X, [Required] double Y, [Required] int Page); + +/// +/// DTO für Empfänger, die erstellt oder abgerufen werden sollen. +/// Wenn nicht, wird sie erstellt und mit einer Signatur versehen. +/// +/// Unterschriften auf Dokumenten. +/// Der Name, mit dem der Empfänger angesprochen werden soll. Bei Null oder keinem Wert wird der zuletzt verwendete Name verwendet. +/// Sollte mit Vorwahl geschrieben werden +public record ReceiverGetOrCreateCommand([Required] IEnumerable Signatures, string? Salution = null, string? PhoneNumber = null) +{ + private string _emailAddress = string.Empty; + + /// + /// E-Mail-Adresse des Empfängers. + /// + [Required] + public string EmailAddress { get => _emailAddress.ToLower(); init => _emailAddress = value.ToLower(); } +}; + +/// +/// DTO zum Erstellen eines Dokuments. +/// +public record DocumentCreateCommand() +{ + /// + /// Die Dokumentdaten im Base64-String-Format. Wird verwendet, wenn das Dokument als Base64-codierter String bereitgestellt wird. + /// + [Required] + public required string DataAsBase64 { get; init; } +}; +#endregion \ No newline at end of file diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverMappingProfile.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverMappingProfile.cs new file mode 100644 index 00000000..0f49aaed --- /dev/null +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverMappingProfile.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using EnvelopeGenerator.Application.DTOs.Receiver; +using EnvelopeGenerator.Application.Envelopes.Commands; +using EnvelopeGenerator.Domain.Entities; + +namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; + +/// +/// +/// +public class CreateEnvelopeReceiverMappingProfile : Profile +{ + /// + /// + /// + public CreateEnvelopeReceiverMappingProfile() + { + CreateMap(); + CreateMap(); + } +} diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverResponse.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverResponse.cs new file mode 100644 index 00000000..251e6ceb --- /dev/null +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Commands/Create/CreateEnvelopeReceiverResponse.cs @@ -0,0 +1,39 @@ +using DigitalData.UserManager.Domain.Entities; +using EnvelopeGenerator.Application.DTOs.Receiver; +using EnvelopeGenerator.Application.Envelopes.Commands; + +namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; + +/// +/// +/// +public record CreateEnvelopeReceiverResponse : CreateEnvelopeResponse +{ + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public CreateEnvelopeReceiverResponse(int Id, int UserId, int Status, string Uuid, string? Message, DateTime AddedWhen, DateTime? ChangedWhen, string? Title, string Language, bool TFAEnabled, User User) : base(Id, UserId, Status, Uuid, Message, AddedWhen, ChangedWhen, Title, Language, TFAEnabled, User) + { + } + + /// + /// + /// + public IEnumerable SentReceiver { get; set; } = new List(); + + /// + /// + /// + public IEnumerable UnsentReceivers { get; set; } = new List(); +} diff --git a/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommand.cs b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommand.cs new file mode 100644 index 00000000..bf267203 --- /dev/null +++ b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommand.cs @@ -0,0 +1,26 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace EnvelopeGenerator.Application.Envelopes.Commands; + +/// +/// Befehl zur Erstellung eines Umschlags. +/// +/// Der Titel des Umschlags. Dies ist ein Pflichtfeld. +/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld. +/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false. +public record CreateEnvelopeCommand( + [Required] string Title, + [Required] string Message, + bool TFAEnabled = false + ) : IRequest +{ + /// + /// Id of receiver + /// + [JsonIgnore] + [BindNever] + public int? UserId { get; set; } +}; diff --git a/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommandHandler.cs b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommandHandler.cs new file mode 100644 index 00000000..5c8cb3e9 --- /dev/null +++ b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommandHandler.cs @@ -0,0 +1,41 @@ +using AutoMapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using MediatR; + +namespace EnvelopeGenerator.Application.Envelopes.Commands; + +/// +/// +/// +public class CreateEnvelopeCommandHandler : IRequestHandler +{ + private readonly IEnvelopeExecutor _envelopeExecutor; + + private readonly IMapper _mapper; + + /// + /// + /// + /// + /// + public CreateEnvelopeCommandHandler(IEnvelopeExecutor envelopeExecutor, IMapper mapper) + { + _envelopeExecutor = envelopeExecutor; + _mapper = mapper; + } + + /// + /// + /// + /// + /// + /// + public async Task Handle(CreateEnvelopeCommand request, CancellationToken cancellationToken) + { + int userId = request.UserId ?? throw new InvalidOperationException("UserId cannot be null when creating an envelope."); + + var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancellationToken); + + return _mapper.Map(envelope); + } +} diff --git a/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeMappingProfile.cs b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeMappingProfile.cs new file mode 100644 index 00000000..9cf72359 --- /dev/null +++ b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeMappingProfile.cs @@ -0,0 +1,19 @@ +using AutoMapper; +using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; +using EnvelopeGenerator.Domain.Entities; + +namespace EnvelopeGenerator.Application.Envelopes.Commands; + +/// +/// +/// +public class CreateEnvelopeMappingProfile : Profile +{ + /// + /// + /// + public CreateEnvelopeMappingProfile() + { + CreateMap(); + } +} diff --git a/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeResponse.cs b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeResponse.cs new file mode 100644 index 00000000..998b80e0 --- /dev/null +++ b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeResponse.cs @@ -0,0 +1,20 @@ +using EnvelopeGenerator.Application.Envelopes.Queries.Read; + +namespace EnvelopeGenerator.Application.Envelopes.Commands; + +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +public record CreateEnvelopeResponse(int Id, int UserId, int Status, string Uuid, string? Message, DateTime AddedWhen, DateTime? ChangedWhen, string? Title, string Language, bool TFAEnabled, DigitalData.UserManager.Domain.Entities.User User) + : ReadEnvelopeResponse(Id, UserId, Status, Uuid, Message, AddedWhen, ChangedWhen, Title, Language, TFAEnabled, User); diff --git a/EnvelopeGenerator.Application/Envelopes/Queries/ReceiverName/ReadReceiverNameQuery.cs b/EnvelopeGenerator.Application/Envelopes/Queries/ReceiverName/ReadReceiverNameQuery.cs index fe114dd8..55b477a3 100644 --- a/EnvelopeGenerator.Application/Envelopes/Queries/ReceiverName/ReadReceiverNameQuery.cs +++ b/EnvelopeGenerator.Application/Envelopes/Queries/ReceiverName/ReadReceiverNameQuery.cs @@ -6,8 +6,6 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries.ReceiverName; /// Eine Abfrage, um die zuletzt verwendete Anrede eines Empfängers zu ermitteln, /// damit diese für zukünftige Umschläge wiederverwendet werden kann. /// -/// Der Umschlag, für den die Anrede des Empfängers ermittelt werden soll. -/// Gibt an, ob nur die zuletzt verwendete Anrede zurückgegeben werden soll. -public record ReadReceiverNameQuery(EnvelopeQuery? Envelope = null, bool OnlyLast = true) : ReadReceiverQuery +public record ReadReceiverNameQuery() : ReadReceiverQuery { } diff --git a/EnvelopeGenerator.Application/Exceptions/BadRequestException.cs b/EnvelopeGenerator.Application/Exceptions/BadRequestException.cs new file mode 100644 index 00000000..ce45ba75 --- /dev/null +++ b/EnvelopeGenerator.Application/Exceptions/BadRequestException.cs @@ -0,0 +1,22 @@ +namespace EnvelopeGenerator.Application.Exceptions; + +/// +/// Represents an exception that is thrown when a bad request is encountered. +/// +public class BadRequestException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public BadRequestException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public BadRequestException(string? message) : base(message) + { + } +} diff --git a/EnvelopeGenerator.Application/Exceptions/NotFoundException.cs b/EnvelopeGenerator.Application/Exceptions/NotFoundException.cs new file mode 100644 index 00000000..67b58ef3 --- /dev/null +++ b/EnvelopeGenerator.Application/Exceptions/NotFoundException.cs @@ -0,0 +1,22 @@ +namespace EnvelopeGenerator.Application.Exceptions; + +/// +/// Represents an exception that is thrown when a requested resource is not found. +/// +public class NotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotFoundException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public NotFoundException(string? message) : base(message) + { + } +} diff --git a/EnvelopeGenerator.Application/Histories/Queries/Read/ReadHistoryQuery.cs b/EnvelopeGenerator.Application/Histories/Queries/Read/ReadHistoryQuery.cs index 2f8ba313..7439b900 100644 --- a/EnvelopeGenerator.Application/Histories/Queries/Read/ReadHistoryQuery.cs +++ b/EnvelopeGenerator.Application/Histories/Queries/Read/ReadHistoryQuery.cs @@ -4,17 +4,14 @@ using EnvelopeGenerator.Common; namespace EnvelopeGenerator.Application.Histories.Queries.Read; +//TODO: Add sender query /// /// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags. /// /// Die eindeutige Kennung des Umschlags. -/// Die Abfrage, die den Umschlag beschreibt. -/// Die Abfrage, die den Empfänger beschreibt. /// Abfrage, die angibt, worauf sich der Datensatz bezieht. Ob er sich auf den Empfänger, den Sender oder das System bezieht, wird durch 0, 1 bzw. 2 dargestellt. /// Abfrage zur Steuerung, ob nur der aktuelle Status oder der gesamte Datensatz zurückgegeben wird. public record ReadHistoryQuery( - int EnvelopeId, - ReadEnvelopeQuery? Envelope = null, - ReadReceiverQuery? Receiver = null, + int? EnvelopeId = null, Constants.ReferenceType? Related = null, bool? OnlyLast = true); \ No newline at end of file diff --git a/EnvelopeGenerator.Application/SQL/DocumentCreateReadSQL.cs b/EnvelopeGenerator.Application/SQL/DocumentCreateReadSQL.cs new file mode 100644 index 00000000..ed682b0e --- /dev/null +++ b/EnvelopeGenerator.Application/SQL/DocumentCreateReadSQL.cs @@ -0,0 +1,42 @@ +using Dapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Domain.Entities; + +namespace EnvelopeGenerator.Application.SQL; + +/// +/// +/// +public class DocumentCreateReadSQL : ISQL +{ + /// + /// Base64, OUT_UID + /// + public string Raw => @" + DECLARE @BYTE_DATA1 as VARBINARY(MAX) + SET @BYTE_DATA1 = @ByteData + DECLARE @OUT_DOCID int + + EXEC [dbo].[PRSIG_API_ADD_DOC] + {0}, + @BYTE_DATA1, + @OUT_DOCID OUTPUT + + SELECT TOP(1) * + FROM [dbo].[TBSIG_ENVELOPE_DOCUMENT] + WHERE [GUID] = @OUT_DOCID + "; + + /// + /// + /// + /// + /// + public static DynamicParameters CreateParmas(string base64) + { + var parameters = new DynamicParameters(); + byte[] byteData = Convert.FromBase64String(base64); + parameters.Add("ByteData", byteData, System.Data.DbType.Binary); + return parameters; + } +} diff --git a/EnvelopeGenerator.Application/SQL/EnvelopeCreateReadSQL.cs b/EnvelopeGenerator.Application/SQL/EnvelopeCreateReadSQL.cs new file mode 100644 index 00000000..40b05e68 --- /dev/null +++ b/EnvelopeGenerator.Application/SQL/EnvelopeCreateReadSQL.cs @@ -0,0 +1,49 @@ +using Dapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Domain.Entities; +using System.Data; + +namespace EnvelopeGenerator.Application.SQL; + +/// +/// +/// +public class EnvelopeCreateReadSQL : ISQL +{ + /// + /// USER_ID, TITLE, TFAEnabled, MESSAGE + /// + public string Raw => @" + DECLARE @OUT_UID varchar(36); + + EXEC [dbo].[PRSIG_API_CREATE_ENVELOPE] + {0}, + {1}, + {2}, + {3}, + @OUT_UID OUTPUT; + + SELECT TOP(1) * + FROM [dbo].[TBSIG_ENVELOPE] + WHERE [ENVELOPE_UUID] = @OUT_UID; + "; + + /// + /// + /// + /// + /// + /// + /// + /// + public static DynamicParameters CreateParams(int userId, string title = "", string message = "", bool tfaEnabled = false) + { + var parameters = new DynamicParameters(); + parameters.Add("@UserId", userId); + parameters.Add("@Title", title); + parameters.Add("@TfaEnabled", tfaEnabled ? 1 : 0); + parameters.Add("@Message", message); + parameters.Add("@OutUid", dbType: DbType.String, size: 36, direction: ParameterDirection.Output); + return parameters; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/SQL/EnvelopeReceiverAddReadSQL.cs b/EnvelopeGenerator.Application/SQL/EnvelopeReceiverAddReadSQL.cs new file mode 100644 index 00000000..73fc5de2 --- /dev/null +++ b/EnvelopeGenerator.Application/SQL/EnvelopeReceiverAddReadSQL.cs @@ -0,0 +1,47 @@ +using Dapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Domain.Entities; + +namespace EnvelopeGenerator.Application.SQL; + +/// +/// +/// +public class EnvelopeReceiverAddReadSQL : ISQL +{ + /// + /// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE, + /// + public string Raw => @" + DECLARE @OUT_RECEIVER_ID int + + EXEC [dbo].[PRSIG_API_CREATE_RECEIVER] + {0}, + {1}, + {2}, + {3}, + @OUT_RECEIVER_ID OUTPUT + + SELECT TOP(1) [ENVELOPE_ID] As EnvelopeId, [RECEIVER_ID] As ReceiverId + FROM [dbo].[TBSIG_ENVELOPE_RECEIVER] + WHERE [GUID] = @OUT_RECEIVER_ID; + "; + + /// + /// + /// + /// + /// + /// + /// + /// + public static DynamicParameters CreateParameters(string envelope_uuid, string emailAddress, string? salutation = null, string? phone = null) + { + var parameters = new DynamicParameters(); + parameters.Add("@ENV_UID", envelope_uuid); + parameters.Add("@EMAIL_ADRESS", emailAddress); + parameters.Add("@SALUTATION", salutation); + parameters.Add("@PHONE", phone); + return parameters; + } +} diff --git a/EnvelopeGenerator.Application/SQL/ParamsExtensions.cs b/EnvelopeGenerator.Application/SQL/ParamsExtensions.cs new file mode 100644 index 00000000..9f8ad1ec --- /dev/null +++ b/EnvelopeGenerator.Application/SQL/ParamsExtensions.cs @@ -0,0 +1,30 @@ +using System.Globalization; + +namespace EnvelopeGenerator.Application.SQL; + +/// +/// Extension method for converting objects to SQL parameter strings. +/// +public static class ParamsExtensions +{ + /// + /// Converts a .NET object to its corresponding SQL-safe parameter string. + /// + /// The object to convert. + /// A string representing the SQL parameter. + public static string ToSqlParam(this object? obj) + { + if (obj is null) + return "NULL"; + else if (obj is string strVal) + return $"'{strVal}'"; + else if (obj is bool boolVal) + return boolVal ? "1" : "0"; + else if (obj is double doubleVal) + return $"'{doubleVal.ToString(CultureInfo.InvariantCulture)}'"; + else if (obj is int intVal) + return intVal.ToString(); + else + throw new NotSupportedException($"Type '{obj.GetType().FullName}' is not supported for SQL parameter conversion."); + } +} diff --git a/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs b/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs index ddcd3e2a..dcde6d75 100644 --- a/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs +++ b/EnvelopeGenerator.Application/Services/EnvelopeReceiverService.cs @@ -10,6 +10,8 @@ using Microsoft.Extensions.Logging; using EnvelopeGenerator.Extensions; using EnvelopeGenerator.Application.DTOs.Messaging; using EnvelopeGenerator.Application.Contracts.Services; +using EnvelopeGenerator.Application.Envelopes; +using EnvelopeGenerator.Application.Receivers.Queries.Read; namespace EnvelopeGenerator.Application.Services; @@ -137,9 +139,25 @@ public class EnvelopeReceiverService : BasicCRUDService>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses) + public async Task>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, EnvelopeQuery? envelopeQuery = null, ReadReceiverQuery? receiverQuery = null, params int[] ignore_statuses) { var er_list = await _repository.ReadByUsernameAsync(username: username, min_status: min_status, max_status: max_status, ignore_statuses: ignore_statuses); + + if(envelopeQuery?.Id is int eId) + er_list = er_list.Where(er => er.EnvelopeId == eId); + + if (envelopeQuery?.Uuid is string uuid) + er_list = er_list.Where(er => er.Envelope?.Uuid == uuid); + + if (envelopeQuery?.Status is int status) + er_list = er_list.Where(er => er.Envelope?.Status == status); + + if(receiverQuery?.Id is int id) + er_list = er_list.Where(er => er.Receiver?.Id == id); + + if (receiverQuery?.Signature is string signature) + er_list = er_list.Where(er => er.Receiver?.Signature == signature); + var dto_list = _mapper.Map>(er_list); return Result.Success(dto_list); } diff --git a/EnvelopeGenerator.Common/Constants.vb b/EnvelopeGenerator.Common/Constants.vb index 59570ee1..2e61cefb 100644 --- a/EnvelopeGenerator.Common/Constants.vb +++ b/EnvelopeGenerator.Common/Constants.vb @@ -21,7 +21,6 @@ DocumentOpened = 2004 DocumentSigned = 2005 DocumentForwarded = 4001 - SignatureConfirmed = 2006 DocumentRejected = 2007 EnvelopeShared = 2008 EnvelopeViewed = 2009 @@ -33,6 +32,20 @@ DocumentMod_Rotation = 4001 End Enum + Public Class Status + Public Shared ReadOnly NonHist As IReadOnlyList(Of EnvelopeStatus) = New List(Of EnvelopeStatus) From { + EnvelopeStatus.Invalid, + EnvelopeStatus.EnvelopeSaved, + EnvelopeStatus.EnvelopeSent, + EnvelopeStatus.EnvelopePartlySigned + } + + Public Shared ReadOnly RelatedToFormApp As IReadOnlyList(Of EnvelopeStatus) = New List(Of EnvelopeStatus) From { + EnvelopeStatus.EnvelopeCreated, + EnvelopeStatus.DocumentMod_Rotation + } + End Class + 'TODO: standardize in xwiki Public Enum ReferenceType Receiver = 0 diff --git a/EnvelopeGenerator.Domain/Entities/EmailTemplate.cs b/EnvelopeGenerator.Domain/Entities/EmailTemplate.cs index aef2df9d..689df6b9 100644 --- a/EnvelopeGenerator.Domain/Entities/EmailTemplate.cs +++ b/EnvelopeGenerator.Domain/Entities/EmailTemplate.cs @@ -1,4 +1,5 @@ using DigitalData.Core.Abstractions; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -20,5 +21,13 @@ namespace EnvelopeGenerator.Domain.Entities [Column("SUBJECT", TypeName = "nvarchar(512)")] public string? Subject { get; set; } + + [Required] + [Column("ADDED_WHEN", TypeName = "datetime")] + [DefaultValue("GETDATE()")] + public DateTime AddedWhen { get; set; } + + [Column("CHANGED_WHEN", TypeName = "datetime")] + public DateTime? ChangedWhen { get; set; } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Domain/Entities/Envelope.cs b/EnvelopeGenerator.Domain/Entities/Envelope.cs index e4db872e..1711113f 100644 --- a/EnvelopeGenerator.Domain/Entities/Envelope.cs +++ b/EnvelopeGenerator.Domain/Entities/Envelope.cs @@ -28,7 +28,6 @@ namespace EnvelopeGenerator.Domain.Entities [Column("ENVELOPE_UUID", TypeName = "nvarchar(36)")] public required string Uuid { get; init; } - [Required] [Column("MESSAGE", TypeName = "nvarchar(max)")] public string? Message { get; set; } @@ -52,7 +51,7 @@ namespace EnvelopeGenerator.Domain.Entities public int? ContractType { get; set; } [Column("LANGUAGE", TypeName = "nvarchar(5)")] - public required string Language { get; set; } + public string? Language { get; set; } [Column("SEND_REMINDER_EMAILS")] public bool? SendReminderEmails { get; set; } @@ -87,6 +86,9 @@ namespace EnvelopeGenerator.Domain.Entities [Column("TFA_ENABLED", TypeName = "bit")] public bool TFAEnabled { get; set; } + [Column("DOC_RESULT", TypeName = "varbinary(max)")] + public byte[]? DocResult { get; init; } + /// /// The sender of envelope /// diff --git a/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj b/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj index cb187fa2..dbaad97c 100644 --- a/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj +++ b/EnvelopeGenerator.Extensions/EnvelopeGenerator.Extensions.csproj @@ -13,6 +13,7 @@ + diff --git a/EnvelopeGenerator.Extensions/MemoryCacheExtensions.cs b/EnvelopeGenerator.Extensions/MemoryCacheExtensions.cs new file mode 100644 index 00000000..86ac56bb --- /dev/null +++ b/EnvelopeGenerator.Extensions/MemoryCacheExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace EnvelopeGenerator.Extensions; + +public static class MemoryCacheExtensions +{ + private static readonly Guid BaseId = Guid.NewGuid(); + + public static IDictionary GetEnumAsDictionary(this IMemoryCache memoryCache) + where TEnum : Enum + => memoryCache.GetOrCreate(BaseId + typeof(TEnum).FullName, _ => + Enum.GetValues(typeof(TEnum)) + .Cast() + .ToDictionary(e => e.ToString(), e => Convert.ToInt32(e))) + ?? throw new InvalidOperationException($"Failed to cache or retrieve enum dictionary for type '{typeof(TEnum).FullName}'."); +} diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs index a5e8f748..23b9bfdb 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs @@ -118,16 +118,8 @@ public partial class AuthController : ControllerBase [HttpPost("logout")] public async Task Logout() { - try - { - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error occurred.\n{ErrorMessage}", ex.Message); - return StatusCode(StatusCodes.Status500InternalServerError); - } + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Ok(); } /// diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/ControllerExtensions.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/ControllerExtensions.cs index 1121a9b8..beac46da 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/ControllerExtensions.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/ControllerExtensions.cs @@ -3,22 +3,60 @@ using System.Security.Claims; namespace EnvelopeGenerator.GeneratorAPI.Controllers { + /// + /// Provides extension methods for extracting user information from a . + /// public static class ControllerExtensions { - public static int? GetId(this ClaimsPrincipal user) - => int.TryParse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value, out int result) + /// + /// Attempts to retrieve the user's ID from the claims. Returns null if the ID is not found or invalid. + /// + /// The representing the user. + /// The user's ID as an integer, or null if not found or invalid. + public static int? GetIdOrDefault(this ClaimsPrincipal user) + => int.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.FindFirstValue("sub"), out int result) ? result : null; - - public static string? GetUsername(this ClaimsPrincipal user) + + /// + /// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid. + /// + /// The representing the user. + /// The user's ID as an integer. + /// Thrown if the user ID claim is missing or invalid. + public static int GetId(this ClaimsPrincipal user) + => user.GetIdOrDefault() + ?? throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token."); + + /// + /// Retrieves the username from the claims, if available. + /// + /// The representing the user. + /// The username as a string, or null if not found. + public static string? GetUsernameOrDefault(this ClaimsPrincipal user) => user.FindFirst(ClaimTypes.Name)?.Value; - - public static string? GetName(this ClaimsPrincipal user) + + /// + /// Retrieves the user's surname (last name) from the claims, if available. + /// + /// The representing the user. + /// The surname as a string, or null if not found. + public static string? GetNameOrDefault(this ClaimsPrincipal user) => user.FindFirst(ClaimTypes.Surname)?.Value; - - public static string? GetPrename(this ClaimsPrincipal user) + + /// + /// Retrieves the user's given name (first name) from the claims, if available. + /// + /// The representing the user. + /// The given name as a string, or null if not found. + public static string? GetPrenameOrDefault(this ClaimsPrincipal user) => user.FindFirst(ClaimTypes.GivenName)?.Value; - - public static string? GetEmail(this ClaimsPrincipal user) + + /// + /// Retrieves the user's email address from the claims, if available. + /// + /// The representing the user. + /// The email address as a string, or null if not found. + public static string? GetEmailOrDefault(this ClaimsPrincipal user) => user.FindFirst(ClaimTypes.Email)?.Value; } } \ No newline at end of file diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/EmailTemplateController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/EmailTemplateController.cs index fa4fb84a..41927e85 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/EmailTemplateController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/EmailTemplateController.cs @@ -5,11 +5,17 @@ using EnvelopeGenerator.Application.EmailTemplates.Commands.Reset; using EnvelopeGenerator.Application.EmailTemplates.Queries.Read; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using EnvelopeGenerator.Application.Contracts.Repositories; +using EnvelopeGenerator.Application.DTOs; +using MediatR; +using System.Threading.Tasks; +using DigitalData.UserManager.Application.Services; +using EnvelopeGenerator.Application.Exceptions; namespace EnvelopeGenerator.GeneratorAPI.Controllers; /// -/// Controller for managing email templates. +/// Controller for managing temp templates. /// Steuerung zur Verwaltung von E-Mail-Vorlagen. /// [Route("api/[controller]")] @@ -17,17 +23,27 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers; [Authorize] public class EmailTemplateController : ControllerBase { + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly IEmailTemplateRepository _repository; + + private readonly IMediator _mediator; + /// /// Initialisiert eine neue Instanz der -Klasse. /// /// + /// /// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird. /// - public EmailTemplateController(IMapper mapper) + public EmailTemplateController(IMapper mapper, IEmailTemplateRepository repository, ILogger logger, IMediator mediator) { _mapper = mapper; + _repository = repository; + _logger = logger; + _mediator = mediator; } /// @@ -45,17 +61,25 @@ public class EmailTemplateController : ControllerBase /// Wenn der Benutzer nicht authentifiziert ist. /// Wenn die gesuchte Abfrage nicht gefunden wird. [HttpGet] - public IActionResult Get([FromQuery] ReadEmailTemplateQuery? emailTemplate = null) + public async Task Get([FromQuery] ReadEmailTemplateQuery? emailTemplate = null) { - // Implementation logic here - return Ok(); // Placeholder for actual implementation + if (emailTemplate is null || (emailTemplate.Id is null && emailTemplate.Type is null)) + { + var temps = await _repository.ReadAllAsync(); + return Ok(_mapper.Map>(temps)); + } + else + { + var temp = await _mediator.Send(emailTemplate); + return temp is null ? NotFound() : Ok(temp); + } } /// - /// Updates an email template or resets it if no update command is provided. + /// Updates an temp template or resets it if no update command is provided. /// Aktualisiert eine E-Mail-Vorlage oder setzt sie zurück, wenn kein Aktualisierungsbefehl angegeben ist. /// - /// Die E-Mail-Vorlagenabfrage. + /// Die E-Mail-Vorlagenabfrage. /// Der Aktualisierungsbefehl für die E-Mail-Vorlage. /// Wird auf Standardwert aktualisiert, wenn die Anfrage ohne http-Body gesendet wird. /// @@ -73,19 +97,22 @@ public class EmailTemplateController : ControllerBase /// Wenn der Benutzer nicht authentifiziert ist. /// Wenn die gesuchte Abfrage nicht gefunden wird. [HttpPut] - public IActionResult Update([FromQuery] EmailTemplateQuery email, [FromBody] UpdateEmailTemplateCommand? update = null) + public async Task Update([FromQuery] EmailTemplateQuery? temp = null, [FromBody] UpdateEmailTemplateCommand? update = null) { if (update is null) { - var reset = _mapper.Map(email); - // Logic for resetting the email template + await _mediator.Send(new ResetEmailTemplateCommand(temp)); + return Ok(); + } + else if (temp is null) + { + return BadRequest("No both id and type"); } else { - update.EmailTemplateQuery = email; - // Logic for updating the email template + update.EmailTemplateQuery = temp; + await _mediator.Send(update); + return Ok(); } - - return Ok(); // Placeholder for actual implementation } } diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeController.cs index 049c0186..9deca564 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeController.cs @@ -1,8 +1,13 @@ using DigitalData.Core.DTO; using EnvelopeGenerator.Application.Contracts.Services; +using EnvelopeGenerator.Application.Envelopes.Commands; using EnvelopeGenerator.Application.Envelopes.Queries.Read; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations.Schema; namespace EnvelopeGenerator.GeneratorAPI.Controllers; @@ -27,16 +32,19 @@ public class EnvelopeController : ControllerBase { private readonly ILogger _logger; private readonly IEnvelopeService _envelopeService; + private readonly IMediator _mediator; /// /// Erstellt eine neue Instanz des EnvelopeControllers. /// /// Der Logger, der für das Protokollieren von Informationen verwendet wird. /// Der Dienst, der für die Verarbeitung von Umschlägen zuständig ist. - public EnvelopeController(ILogger logger, IEnvelopeService envelopeService) + /// + public EnvelopeController(ILogger logger, IEnvelopeService envelopeService, IMediator mediator) { _logger = logger; _envelopeService = envelopeService; + _mediator = mediator; } /// @@ -53,26 +61,97 @@ public class EnvelopeController : ControllerBase [HttpGet] public async Task GetAsync([FromQuery] ReadEnvelopeQuery envelope) { - try + if (User.GetId() is int intId) + return await _envelopeService.ReadByUserAsync(intId, min_status: envelope.Status, max_status: envelope.Status).ThenAsync( + Success: envelopes => + { + if (envelope.Id is int id) + envelopes = envelopes.Where(e => e.Id == id); + + if (envelope.Status is int status) + envelopes = envelopes.Where(e => e.Status == status); + + if (envelope.Uuid is string uuid) + envelopes = envelopes.Where(e => e.Uuid == uuid); + + return Ok(envelopes); + }, + Fail: IActionResult (msg, ntc) => + { + _logger.LogNotice(ntc); + return StatusCode(StatusCodes.Status500InternalServerError); + }); + else { - if (User.GetId() is int intId) - return await _envelopeService.ReadByUserAsync(intId, min_status: envelope.Status, max_status: envelope.Status).ThenAsync( - Success: Ok, - Fail: IActionResult (msg, ntc) => - { - _logger.LogNotice(ntc); - return StatusCode(StatusCodes.Status500InternalServerError); - }); - else - { - _logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein."); - return StatusCode(StatusCodes.Status500InternalServerError); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "{Message}", ex.Message); + _logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein."); return StatusCode(StatusCodes.Status500InternalServerError); } } + + /// + /// Ruft das Ergebnis eines Dokuments basierend auf der ID ab. + /// + /// Die eindeutige ID des Umschlags. + /// Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false). + /// Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält. + /// Das Dokument wurde erfolgreich abgerufen. + /// Das Dokument wurde nicht gefunden oder ist nicht verfügbar. + /// Ein unerwarteter Fehler ist aufgetreten. + [HttpGet("doc-result")] + public async Task GetDocResultAsync([FromQuery] int id, [FromQuery] bool view = false) + { + if (User.GetId() is int intId) + return await _envelopeService.ReadByUserAsync(intId).ThenAsync( + Success: envelopes => + { + var envelope = envelopes.Where(e => e.Id == id).FirstOrDefault(); + + if (envelope is null) + return NotFound("Envelope not available."); + else if (envelope?.DocResult is null) + return NotFound("The document has not been fully signed or the result has not yet been released."); + else + { + if (view) + { + Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\""); + return File(envelope.DocResult, "application/pdf"); + } + else + return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf"); + } + }, + Fail: IActionResult (msg, ntc) => + { + _logger.LogNotice(ntc); + return StatusCode(StatusCodes.Status500InternalServerError); + }); + else + { + _logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein."); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + /// + /// + /// + /// + /// + [NonAction] + [Authorize] + [HttpPost] + public async Task CreateAsync([FromQuery] CreateEnvelopeCommand envelope) + { + envelope.UserId = User.GetId(); + var res = await _mediator.Send(envelope); + + if (res is null) + { + _logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(envelope)); + return StatusCode(StatusCodes.Status500InternalServerError); + } + else + return Ok(res); + } } diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeReceiverController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeReceiverController.cs index dd8e828a..2fa2245b 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeReceiverController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeReceiverController.cs @@ -1,11 +1,20 @@ -using DigitalData.Core.DTO; +using AutoMapper; +using DigitalData.Core.DTO; using EnvelopeGenerator.Application.Contracts.Services; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Application.DTOs.Receiver; using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create; using EnvelopeGenerator.Application.EnvelopeReceivers.Queries.Read; using EnvelopeGenerator.Application.Envelopes.Queries.ReceiverName; +using EnvelopeGenerator.Application.SQL; +using EnvelopeGenerator.Domain.Entities; +using EnvelopeGenerator.GeneratorAPI.Models; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Options; +using System.Data; namespace EnvelopeGenerator.GeneratorAPI.Controllers; @@ -26,17 +35,37 @@ public class EnvelopeReceiverController : ControllerBase private readonly IMediator _mediator; + private readonly IMapper _mapper; + + private readonly IEnvelopeExecutor _envelopeExecutor; + + private readonly IEnvelopeReceiverExecutor _erExecutor; + + private readonly IDocumentExecutor _documentExecutor; + + private readonly string _cnnStr; + /// /// Konstruktor für den EnvelopeReceiverController. /// /// Logger-Instanz zur Protokollierung von Informationen und Fehlern. /// Service zur Verwaltung von Umschlagempfängern. /// Mediator-Instanz zur Verarbeitung von Befehlen und Abfragen. - public EnvelopeReceiverController(ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator) + /// + /// + /// + /// + /// + public EnvelopeReceiverController(ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions csOpt) { _logger = logger; _erService = envelopeReceiverService; _mediator = mediator; + _mapper = mapper; + _envelopeExecutor = envelopeExecutor; + _erExecutor = erExecutor; + _documentExecutor = documentExecutor; + _cnnStr = csOpt.Value.Value; } /// @@ -55,30 +84,29 @@ public class EnvelopeReceiverController : ControllerBase [HttpGet] public async Task GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver) { - try - { - var username = User.GetUsername(); + var username = User.GetUsernameOrDefault(); - if (username is null) - { - _logger.LogError(@"Envelope Receiver dto cannot be sent because username claim is null. Potential authentication and authorization error. The value of other claims are [id: {id}], [username: {username}], [name: {name}], [prename: {prename}], [email: {email}].", - User.GetId(), User.GetUsername(), User.GetName(), User.GetPrename(), User.GetEmail()); - return StatusCode(StatusCodes.Status500InternalServerError); - } - - return await _erService.ReadByUsernameAsync(username: username).ThenAsync( - Success: Ok, - Fail: IActionResult (msg, ntc) => - { - _logger.LogNotice(ntc); - return StatusCode(StatusCodes.Status500InternalServerError, msg); - }); - } - catch (Exception ex) + if (username is null) { - _logger.LogError(ex, "An unexpected error occurred. {message}", ex.Message); - return new StatusCodeResult(StatusCodes.Status500InternalServerError); + _logger.LogError(@"Envelope Receiver dto cannot be sent because username claim is null. Potential authentication and authorization error. The value of other claims are [id: {id}], [username: {username}], [name: {name}], [prename: {prename}], [email: {email}].", + User.GetId(), User.GetUsernameOrDefault(), User.GetNameOrDefault(), User.GetPrenameOrDefault(), User.GetEmailOrDefault()); + return StatusCode(StatusCodes.Status500InternalServerError); } + + return await _erService.ReadByUsernameAsync( + username: username, + min_status: envelopeReceiver.Status?.Min, + max_status: envelopeReceiver.Status?.Max, + envelopeQuery: envelopeReceiver.Envelope, + receiverQuery: envelopeReceiver.Receiver, + ignore_statuses: envelopeReceiver.Status?.Ignore ?? Array.Empty()) + .ThenAsync( + Success: Ok, + Fail: IActionResult (msg, ntc) => + { + _logger.LogNotice(ntc); + return StatusCode(StatusCodes.Status500InternalServerError, msg); + }); } /// @@ -97,31 +125,25 @@ public class EnvelopeReceiverController : ControllerBase [HttpGet("salute")] public async Task GetReceiverName([FromQuery] ReadReceiverNameQuery receiverName) { - try - { - return await _erService.ReadLastUsedReceiverNameByMail(receiverName.EmailAddress).ThenAsync( - Success: res => res is null ? Ok(string.Empty) : Ok(res), - Fail: IActionResult (msg, ntc) => - { - if (ntc.HasFlag(Flag.NotFound)) - return NotFound(); + if (receiverName.EmailAddress is null) + return BadRequest(); - _logger.LogNotice(ntc); - return StatusCode(StatusCodes.Status500InternalServerError); - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "{message}", ex.Message); - return StatusCode(StatusCodes.Status500InternalServerError); - } + return await _erService.ReadLastUsedReceiverNameByMail(receiverName.EmailAddress).ThenAsync( + Success: res => res is null ? Ok(string.Empty) : Ok(res), + Fail: IActionResult (msg, ntc) => + { + if (ntc.HasFlag(Flag.NotFound)) + return NotFound(); + + _logger.LogNotice(ntc); + return StatusCode(StatusCodes.Status500InternalServerError); + }); } /// /// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften. /// - /// - /// Token to cancel the operation + /// /// HTTP-Antwort /// /// Sample request: @@ -157,9 +179,133 @@ public class EnvelopeReceiverController : ControllerBase /// Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden. [Authorize] [HttpPost] - public async Task CreateAsync([FromBody] CreateEnvelopeReceiverCommand createEnvelopeQuery, CancellationToken cancellationToken) + public async Task CreateAsync([FromBody] CreateEnvelopeReceiverCommand request) { - await _mediator.Send(createEnvelopeQuery, cancellationToken); - return Accepted(); + CancellationToken cancel = default; + int userId = User.GetId(); + + #region Create Envelope + var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancel); + #endregion + + #region Add receivers + List sentReceivers = new(); + List unsentReceivers = new(); + + foreach (var receiver in request.Receivers) + { + var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel); + + if (envelopeReceiver is null) + unsentReceivers.Add(receiver); + else + sentReceivers.Add(envelopeReceiver); + } + + var res = _mapper.Map(envelope); + res.UnsentReceivers = unsentReceivers; + res.SentReceiver = _mapper.Map>(sentReceivers.Select(er => er.Receiver)); + #endregion + + #region Add document + var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel); + + if (document is null) + return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed."); + #endregion + + #region Add document element + // @DOC_ID, @RECEIVER_ID, @POSITION_X, @POSITION_Y, @PAGE + string sql = @" + DECLARE @OUT_SUCCESS bit; + + EXEC [dbo].[PRSIG_API_ADD_DOC_RECEIVER_ELEM] + {0}, + {1}, + {2}, + {3}, + {4}, + @OUT_SUCCESS OUTPUT; + + SELECT @OUT_SUCCESS as [@OUT_SUCCESS];"; + + foreach (var rcv in res.SentReceiver) + foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.Signatures ?? Array.Empty()) + { + using (SqlConnection conn = new(_cnnStr)) + { + conn.Open(); + + var formattedSQL = string.Format(sql, document.Id.ToSqlParam(), rcv.Id.ToSqlParam(), sign.X.ToSqlParam(), sign.Y.ToSqlParam(), sign.Page.ToSqlParam()); + + using SqlCommand cmd = new SqlCommand(formattedSQL, conn); + cmd.CommandType = CommandType.Text; + + using SqlDataReader reader = cmd.ExecuteReader(); + if (reader.Read()) + { + bool outSuccess = reader.GetBoolean(0); + } + } + } + #endregion + + #region Create history + // ENV_UID, STATUS_ID, USER_ID, + string sql_hist = @" + USE [DD_ECM] + + DECLARE @OUT_SUCCESS bit; + + EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE] + {0}, + {1}, + {2}, + @OUT_SUCCESS OUTPUT; + + SELECT @OUT_SUCCESS as [@OUT_SUCCESS];"; + + using (SqlConnection conn = new(_cnnStr)) + { + conn.Open(); + var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), userId.ToSqlParam()); + using (SqlCommand cmd = new SqlCommand(formattedSQL_hist, conn)) + { + cmd.CommandType = CommandType.Text; + + using (SqlDataReader reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + bool outSuccess = reader.GetBoolean(0); + } + } + } + } + #endregion + + return Ok(res); } + + /// + /// + /// + /// + /// + public static bool IsBase64String(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return false; + + try + { + Convert.FromBase64String(input); + return true; + } + catch (FormatException) + { + return false; + } + } + } \ No newline at end of file diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeTypeController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeTypeController.cs index bf2ea067..c49d9a3b 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeTypeController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/EnvelopeTypeController.cs @@ -21,20 +21,12 @@ public class EnvelopeTypeController : ControllerBase [HttpGet] public async Task GetAllAsync() { - try - { - return await _service.ReadAllAsync().ThenAsync( - Success: Ok, - Fail: IActionResult (msg, ntc) => - { - _logger.LogNotice(ntc); - return ntc.HasFlag(Flag.NotFound) ? NotFound() : StatusCode(StatusCodes.Status500InternalServerError); - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "{Message}", ex.Message); - return StatusCode(StatusCodes.Status500InternalServerError); - } + return await _service.ReadAllAsync().ThenAsync( + Success: Ok, + Fail: IActionResult (msg, ntc) => + { + _logger.LogNotice(ntc); + return ntc.HasFlag(Flag.NotFound) ? NotFound() : StatusCode(StatusCodes.Status500InternalServerError); + }); } } \ No newline at end of file diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/HistoryController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/HistoryController.cs index 0569bf0d..5c55b567 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/HistoryController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/HistoryController.cs @@ -1,8 +1,10 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; using EnvelopeGenerator.Application.Contracts.Services; using EnvelopeGenerator.Application.Histories.Queries.Read; +using EnvelopeGenerator.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; using static EnvelopeGenerator.Common.Constants; @@ -20,15 +22,18 @@ public class HistoryController : ControllerBase private readonly IEnvelopeHistoryService _service; + private readonly IMemoryCache _memoryCache; + /// /// Konstruktor für den HistoryController. /// /// Der Logger, der für das Protokollieren von Informationen verwendet wird. /// Der Dienst, der für die Verarbeitung der Umschlaghistorie verantwortlich ist. - public HistoryController(ILogger logger, IEnvelopeHistoryService service) + public HistoryController(ILogger logger, IEnvelopeHistoryService service, IMemoryCache memoryCache) { _logger = logger; _service = service; + _memoryCache = memoryCache; } /// @@ -47,19 +52,9 @@ public class HistoryController : ControllerBase /// [HttpGet("related")] [Authorize] - public IActionResult GetReferenceTypes() + public IActionResult GetReferenceTypes(ReferenceType? referenceType = null) { - // Enum zu Schlüssel-Wert-Paar - var referenceTypes = Enum.GetValues(typeof(ReferenceType)) - .Cast() - .ToDictionary(rt => - { - var key = rt.ToString(); - var keyAsCamelCase = char.ToLower(key[0]) + key[1..]; - return keyAsCamelCase; - }, rt => (int)rt); - - return Ok(referenceTypes); + return referenceType is null ? Ok(_memoryCache.GetEnumAsDictionary()) : Ok(referenceType.ToString()); } /// @@ -91,7 +86,7 @@ public class HistoryController : ControllerBase /// 3004: MessageDeletionSent /// 3005: MessageCompletionSent /// - /// + /// /// Abfrageparameter, der angibt, auf welche Referenz sich der Status bezieht. /// 0 - Sender: Historische Datensätze, die sich auf den Status des Absenders beziehen. Sie haben Statuscodes, die mit 1* beginnen. /// 1 - Receiver: Historische Datensätze über den Status der Empfänger. Diese haben Statuscodes, die mit 2* beginnen. @@ -102,19 +97,9 @@ public class HistoryController : ControllerBase /// [HttpGet("status")] [Authorize] - public IActionResult GetEnvelopeStatus([FromQuery] ReferenceType? related = null) + public IActionResult GetEnvelopeStatus([FromQuery] EnvelopeStatus? status = null) { - // Enum zu Schlüssel-Wert-Paar - var referenceTypes = Enum.GetValues(typeof(EnvelopeStatus)) - .Cast() - .ToDictionary(rt => - { - var key = rt.ToString(); - var keyAsCamelCase = char.ToLower(key[0]) + key[1..]; - return keyAsCamelCase; - }, rt => (int)rt); - - return Ok(referenceTypes); + return status is null ? Ok(_memoryCache.GetEnumAsDictionary()) : Ok(status.ToString()); } /// @@ -131,6 +116,8 @@ public class HistoryController : ControllerBase [Authorize] public async Task GetAllAsync([FromQuery] ReadHistoryQuery history) { + + bool withReceiver = false; bool withSender = false; diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/ReceiverController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/ReceiverController.cs index 9920b5de..0062a43f 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/ReceiverController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/ReceiverController.cs @@ -38,24 +38,23 @@ public class ReceiverController : CRUDControllerBaseWithErrorHandling Get([FromQuery] ReadReceiverQuery receiver) { - if (receiver.EmailAddress is null && receiver.Signature is null) + if (receiver.Id is null && receiver.EmailAddress is null && receiver.Signature is null) return await base.GetAll(); - try - { - return await _service.ReadByAsync(emailAddress: receiver.EmailAddress, signature: receiver.Signature).ThenAsync( - Success: Ok, - Fail: IActionResult (msg, ntc) => - { - _logger.LogNotice(ntc); - return StatusCode(StatusCodes.Status500InternalServerError); - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "{Message}", ex.Message); - return StatusCode(StatusCodes.Status500InternalServerError); - } + if (receiver.Id is int id) + return await _service.ReadByIdAsync(id).ThenAsync( + Success: Ok, + Fail: IActionResult (msg, ntc) => + { + return NotFound(); + }); + + return await _service.ReadByAsync(emailAddress: receiver.EmailAddress, signature: receiver.Signature).ThenAsync( + Success: Ok, + Fail: IActionResult (msg, ntc) => + { + return NotFound(); + }); } #region REMOVED ENDPOINTS diff --git a/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj b/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj index 6e922bad..418df9b2 100644 --- a/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj +++ b/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj @@ -10,9 +10,9 @@ Digital Data GmbH Digital Data GmbH EnvelopeGenerator.GeneratorAPI - 1.2.0 - 1.2.0 - 1.2.0 + 1.2.2 + 1.2.2 + 1.2.2 Copyright © 2025 Digital Data GmbH. All rights reserved. bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml diff --git a/EnvelopeGenerator.GeneratorAPI/Middleware/ExceptionHandlingMiddleware.cs b/EnvelopeGenerator.GeneratorAPI/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 00000000..f9d700a1 --- /dev/null +++ b/EnvelopeGenerator.GeneratorAPI/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,84 @@ +namespace EnvelopeGenerator.GeneratorAPI.Middleware; + +using EnvelopeGenerator.Application.Exceptions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Text.Json; + +/// +/// Middleware for handling exceptions globally in the application. +/// Captures exceptions thrown during the request pipeline execution, +/// logs them, and returns an appropriate HTTP response with a JSON error message. +/// +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The next middleware in the request pipeline. + /// The logger instance for logging exceptions. + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + /// + /// Invokes the middleware to handle the HTTP request. + /// + /// The HTTP context of the current request. + /// A task that represents the asynchronous operation. + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); // Continue down the pipeline + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex, _logger); + } + } + + /// + /// Handles exceptions by logging them and writing an appropriate JSON response. + /// + /// The HTTP context of the current request. + /// The exception that occurred. + /// The logger instance for logging the exception. + /// A task that represents the asynchronous operation. + private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger) + { + context.Response.ContentType = "application/json"; + + string message; + + switch (exception) + { + case BadRequestException badRequestEx: + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + message = badRequestEx.Message; + break; + + case NotFoundException notFoundEx: + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + message = notFoundEx.Message; + break; + + default: + logger.LogError(exception, "Unhandled exception occurred."); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + message = "An unexpected error occurred."; + break; + } + + await context.Response.WriteAsync(JsonSerializer.Serialize(new + { + message + })); + } +} diff --git a/EnvelopeGenerator.GeneratorAPI/Models/ConnectionString.cs b/EnvelopeGenerator.GeneratorAPI/Models/ConnectionString.cs new file mode 100644 index 00000000..0adb8e5b --- /dev/null +++ b/EnvelopeGenerator.GeneratorAPI/Models/ConnectionString.cs @@ -0,0 +1,12 @@ +namespace EnvelopeGenerator.GeneratorAPI.Models; + +/// +/// +/// +public class ConnectionString +{ + /// + /// + /// + public string Value { get; set; } = string.Empty; +} diff --git a/EnvelopeGenerator.GeneratorAPI/Models/Login.cs b/EnvelopeGenerator.GeneratorAPI/Models/Login.cs index 7aaeea50..a3f9d30e 100644 --- a/EnvelopeGenerator.GeneratorAPI/Models/Login.cs +++ b/EnvelopeGenerator.GeneratorAPI/Models/Login.cs @@ -6,8 +6,8 @@ namespace EnvelopeGenerator.GeneratorAPI.Models; /// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername. /// /// Das erforderliche Passwort für das Login. -/// Die optionale ID des Benutzers. +/// Die optionale ID des Benutzers. /// Der optionale Benutzername. -public record Login([Required] string Password, int? Id = null, string? Username = null) +public record Login([Required] string Password, int? UserId = null, string? Username = null) { } diff --git a/EnvelopeGenerator.GeneratorAPI/Program.cs b/EnvelopeGenerator.GeneratorAPI/Program.cs index bfdb024b..b41d4b62 100644 --- a/EnvelopeGenerator.GeneratorAPI/Program.cs +++ b/EnvelopeGenerator.GeneratorAPI/Program.cs @@ -15,6 +15,7 @@ using EnvelopeGenerator.GeneratorAPI.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using DigitalData.Core.Abstractions.Security.Extensions; +using EnvelopeGenerator.GeneratorAPI.Middleware; var builder = WebApplication.CreateBuilder(args); @@ -90,6 +91,9 @@ builder.Services.AddSwaggerGen(options => builder.Services.AddOpenApi(); // DbContext var connStr = config.GetConnectionString("Default") ?? throw new InvalidOperationException("There is no default connection string in appsettings.json."); + +builder.Services.Configure(cs => cs.Value = connStr); + builder.Services.AddDbContext(options => options.UseSqlServer(connStr)); builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams")); @@ -159,13 +163,15 @@ builder.Services.AddCookieBasedLocalizer() ; // Envelope generator serives builder.Services - .AddEnvelopeGeneratorRepositories() + .AddEnvelopeGeneratorInfrastructureServices(sqlExecutorConfigureOptions: executor => executor.ConnectionString = connStr) .AddEnvelopeGeneratorServices(config); var app = builder.Build(); deferredProvider.Factory = () => app.Services; +app.UseMiddleware(); + app.MapOpenApi(); // Configure the HTTP request pipeline. diff --git a/EnvelopeGenerator.GeneratorAPI/Properties/launchSettings.json b/EnvelopeGenerator.GeneratorAPI/Properties/launchSettings.json index 8857b777..1b89a1c9 100644 --- a/EnvelopeGenerator.GeneratorAPI/Properties/launchSettings.json +++ b/EnvelopeGenerator.GeneratorAPI/Properties/launchSettings.json @@ -22,9 +22,9 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7174;http://localhost:5131", + "applicationUrl": "https://localhost:8088;http://localhost:5131", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/EnvelopeGenerator.GeneratorAPI/appsettings.json b/EnvelopeGenerator.GeneratorAPI/appsettings.json index c03dbb20..a4ab1aa8 100644 --- a/EnvelopeGenerator.GeneratorAPI/appsettings.json +++ b/EnvelopeGenerator.GeneratorAPI/appsettings.json @@ -36,5 +36,211 @@ "QueryString": "AuthToken", "Issuer": "auth.digitaldata.works", "Audience": "work-flow.digitaldata.works" + }, + "PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==", + "Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value. + "default-src 'self'", + "script-src 'self' 'nonce-{0}' 'unsafe-eval'", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com:*", + "img-src 'self' data: https: blob:", + "font-src 'self' https://fonts.gstatic.com:*", + "connect-src 'self' https://nominatim.openstreetmap.org:* http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* blob:", + "frame-src 'self'", + "media-src 'self'", + "object-src 'self'" + ], + "NLog": { + "throwConfigExceptions": true, + "variables": { + "logDirectory": "E:\\LogFiles\\Digital Data\\signFlow", + "logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Web" + }, + "targets": { + "infoLogs": { + "type": "File", + "fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log", + "maxArchiveDays": 30 + }, + "errorLogs": { + "type": "File", + "fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log", + "maxArchiveDays": 30 + }, + "criticalLogs": { + "type": "File", + "fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log", + "maxArchiveDays": 30 + } + }, + // Trace, Debug, Info, Warn, Error and *Fatal* + "rules": [ + { + "logger": "*", + "minLevel": "Info", + "maxLevel": "Warn", + "writeTo": "infoLogs" + }, + { + "logger": "*", + "level": "Error", + "writeTo": "errorLogs" + }, + { + "logger": "*", + "level": "Fatal", + "writeTo": "criticalLogs" + } + ] + }, + "ContactLink": { + "Label": "Kontakt", + "Href": "https://digitaldata.works/", + "HrefLang": "de", + "Target": "_blank", + "Title": "Digital Data GmbH" + }, + /* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx). + To add a new language, first you should write the required resx file. + first is the default culture name. */ + "Cultures": [ + { + "Language": "de-DE", + "FIClass": "fi-de" + }, + { + "Language": "en-US", + "FIClass": "fi-us" + } + ], + "DisableMultiLanguage": false, + "Regexes": [ + { + "Pattern": "/^\\p{L}+(?:([\\ \\-\\']|(\\.\\ ))\\p{L}+)*$/u", + "Name": "City", + "Platforms": [ ".NET" ] + }, + { + "Pattern": "/^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$/", + "Name": "City", + "Platforms": [ "javascript" ] + } + ], + "CustomImages": { + "App": { + "Src": "/img/DD_signFLOW_LOGO.png", + "Classes": { + "Main": "signFlow-logo" + } + }, + "Company": { + "Src": "/img/digital_data.svg", + "Classes": { + "Show": "dd-show-logo", + "Locked": "dd-locked-logo" + } + } + }, + "DispatcherParams": { + "SendingProfile": 1, + "AddedWho": "DDEnvelopGenerator", + "ReminderTypeId": 202377, + "EmailAttmt1": "" + }, + "MailParams": { + "Placeholders": { + "[NAME_PORTAL]": "signFlow", + "[SIGNATURE_TYPE]": "signieren", + "[REASON]": "" + } + }, + "GtxMessagingParams": { + "Uri": "https://rest.gtx-messaging.net", + "Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json", + "Headers": {}, + "QueryParams": { + "from": "signFlow" + } + }, + "TFARegParams": { + "TimeLimit": "00:30:00" + }, + "DbTriggerParams": { + "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ], + "EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ], + "Receiver": [], + "EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ] + }, + "MainPageTitle": null, + "AnnotationParams": { + "Background": { + "Margin": 0.20, + "BackgroundColor": { + "R": 222, + "G": 220, + "B": 215 + }, + "BorderColor": { + "R": 204, + "G": 202, + "B": 198 + }, + "BorderStyle": "underline", + "BorderWidth": 4 + }, + "DefaultAnnotation": { + "Width": 1, + "Height": 0.5, + "MarginTop": 1 + }, + "Annotations": [ + { + "Name": "Signature", + "MarginTop": 0 + }, + { + "Name": "PositionLabel", + "VerBoundAnnotName": "Signature", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": 0.22 + }, + { + "Name": "Position", + "VerBoundAnnotName": "PositionLabel", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": -0.05 + }, + { + "Name": "CityLabel", + "VerBoundAnnotName": "Position", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": 0.05 + }, + { + "Name": "City", + "VerBoundAnnotName": "CityLabel", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": -0.05 + }, + { + "Name": "DateLabel", + "VerBoundAnnotName": "City", + "WidthRatio": 1.55, + "HeightRatio": 0.5, + "MarginTopRatio": 0.05 + }, + { + "Name": "Date", + "VerBoundAnnotName": "DateLabel", + "WidthRatio": 1.55, + "HeightRatio": 0.5, + "MarginTopRatio": -0.1 + } + ] } } diff --git a/EnvelopeGenerator.Infrastructure/DIExtensions.cs b/EnvelopeGenerator.Infrastructure/DependencyExtensions.cs similarity index 52% rename from EnvelopeGenerator.Infrastructure/DIExtensions.cs rename to EnvelopeGenerator.Infrastructure/DependencyExtensions.cs index c457d264..f7e4adb4 100644 --- a/EnvelopeGenerator.Infrastructure/DIExtensions.cs +++ b/EnvelopeGenerator.Infrastructure/DependencyExtensions.cs @@ -6,6 +6,13 @@ using Microsoft.EntityFrameworkCore; using DigitalData.Core.Infrastructure; using EnvelopeGenerator.Domain.Entities; using DigitalData.Core.Infrastructure.AutoMapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using Microsoft.Extensions.Configuration; +using EnvelopeGenerator.Infrastructure.Executor; +using Dapper; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; +using DigitalData.UserManager.Domain.Entities; namespace EnvelopeGenerator.Infrastructure; @@ -25,7 +32,10 @@ public static class DIExtensions /// This method ensures that the repositories are registered as scoped services, meaning that a new instance of each repository /// will be created per HTTP request (or per scope) within the dependency injection container. /// - public static IServiceCollection AddEnvelopeGeneratorRepositories(this IServiceCollection services, Action? dbContextOptions = null) + public static IServiceCollection AddEnvelopeGeneratorInfrastructureServices(this IServiceCollection services, + Action? dbContextOptions = null, + IConfiguration? sqlExecutorConfiguration = null, + Action? sqlExecutorConfigureOptions = null) { if(dbContextOptions is not null) services.AddDbContext(dbContextOptions); @@ -59,6 +69,92 @@ public static class DIExtensions services.AddDbRepository(context => context.UserReceivers).UseAutoMapper(); services.AddDbRepository(context => context.EnvelopeReceiverReadOnlys).UseAutoMapper(); + services.AddSQLExecutor(); + services.AddSQLExecutor(); + services.AddSQLExecutor(); + services.AddSQLExecutor(); + services.AddSQLExecutor(); + + SetDapperTypeMap(); + SetDapperTypeMap(); + SetDapperTypeMap(); + SetDapperTypeMap(); + SetDapperTypeMap(); + SetDapperTypeMap(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + if (sqlExecutorConfiguration is not null || sqlExecutorConfigureOptions is not null) + services.AddSQLExecutor(sqlExecutorConfiguration, sqlExecutorConfigureOptions); + + return services; + } + + public static IServiceCollection AddSQLExecutor(this IServiceCollection services, IConfiguration? configuration = null, Action? configureOptions = null) + { + if(configuration is not null && configureOptions is not null) + throw new InvalidOperationException("Cannot use both 'configuration' and 'configureOptions'. Only one should be provided."); + + if (configuration is not null) + services.Configure(configuration); + + if(configureOptions is not null) + services.Configure(configureOptions); + + return services.AddSingleton(); + } + + private static void SetDapperTypeMap() + { + Dapper.SqlMapper.SetTypeMap(typeof(TModel), new CustomPropertyTypeMap( + typeof(TModel), + (type, columnName) => + { + return type.GetProperties().FirstOrDefault(prop => + { + var attr = prop.GetCustomAttribute(); + return attr != null && string.Equals(attr.Name, columnName, StringComparison.OrdinalIgnoreCase) + || string.Equals(prop.Name, columnName, StringComparison.OrdinalIgnoreCase); + })!; + } + )); + } + + public static IServiceCollection AddSQLExecutor(this IServiceCollection services, IConfiguration? configuration = null, Action? configureOptions = null) where T : class + { + if (configuration is not null && configureOptions is not null) + throw new InvalidOperationException("Cannot use both 'configuration' and 'configureOptions'. Only one should be provided."); + + if (configuration is not null) + services.Configure(configuration); + + services.AddScoped, SQLExecutor>(); + + var interfaceType = typeof(ISQL<>); + var targetGenericType = interfaceType.MakeGenericType(typeof(T)); + + var implementations = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => + { + try { return a.GetTypes(); } + catch { return Array.Empty(); } + }) + .Where(t => + t is { IsClass: true, IsAbstract: false } && + t.GetInterfaces().Any(i => + i.IsGenericType && + i.GetGenericTypeDefinition() == interfaceType && + i.GenericTypeArguments[0] == typeof(T) + ) + ); + + foreach (var impl in implementations) + { + services.AddSingleton(impl); + } + return services; } } diff --git a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj index f93f27c1..8ad9d9f9 100644 --- a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj +++ b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj @@ -7,6 +7,7 @@ + diff --git a/EnvelopeGenerator.Infrastructure/Executor/DocumentExecutor.cs b/EnvelopeGenerator.Infrastructure/Executor/DocumentExecutor.cs new file mode 100644 index 00000000..11088c63 --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/DocumentExecutor.cs @@ -0,0 +1,29 @@ +using Dapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Application.SQL; +using EnvelopeGenerator.Domain.Entities; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Infrastructure.Executor; + +public class DocumentExecutor : SQLExecutor, IDocumentExecutor +{ + public DocumentExecutor(IServiceProvider provider, IOptions sqlExecutorParamsOptions) : base(provider, sqlExecutorParamsOptions) + { + } + + public async Task CreateDocumentAsync(string base64, string envelope_uuid, CancellationToken cancellation = default) + { + using var connection = new SqlConnection(Params.ConnectionString); + var sql = Provider.GetRequiredService(); + var formattedSql = string.Format(sql.Raw, envelope_uuid.ToSqlParam()); + var param = DocumentCreateReadSQL.CreateParmas(base64); + await connection.OpenAsync(cancellation); + var documents = await connection.QueryAsync(formattedSql, param); + return documents.FirstOrDefault() + ?? throw new InvalidOperationException($"Document creation failed. Parameters:" + + $"base64={base64}, envelope_uuid='{envelope_uuid}'."); + } +} diff --git a/EnvelopeGenerator.Infrastructure/Executor/EnvelopeExecutor.cs b/EnvelopeGenerator.Infrastructure/Executor/EnvelopeExecutor.cs new file mode 100644 index 00000000..945f1ac7 --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/EnvelopeExecutor.cs @@ -0,0 +1,37 @@ +using Dapper; +using DigitalData.UserManager.Application.Contracts.Repositories; +using EnvelopeGenerator.Application.Contracts.Repositories; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Application.SQL; +using EnvelopeGenerator.Domain.Entities; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Infrastructure.Executor; + +public class EnvelopeExecutor : SQLExecutor, IEnvelopeExecutor +{ + private readonly IUserRepository _userRepository; + + public EnvelopeExecutor(IServiceProvider provider, IOptions sqlExecutorParamsOptions, IUserRepository userRepository) : base(provider, sqlExecutorParamsOptions) + { + _userRepository = userRepository; + } + + public async Task CreateEnvelopeAsync(int userId, string title = "", string message = "", bool tfaEnabled = false, CancellationToken cancellation = default) + { + using var connection = new SqlConnection(Params.ConnectionString); + var sql = Provider.GetRequiredService(); + var formattedSql = string.Format(sql.Raw, userId.ToSqlParam(), title.ToSqlParam(), tfaEnabled.ToSqlParam(), message.ToSqlParam()); + await connection.OpenAsync(cancellation); + var envelopes = await connection.QueryAsync(formattedSql); + var envelope = envelopes.FirstOrDefault() + ?? throw new InvalidOperationException($"Envelope creation failed. Parameters:" + + $"userId={userId}, title='{title}', message='{message}', tfaEnabled={tfaEnabled}."); ; + + envelope.User = await _userRepository.ReadByIdAsync(envelope.UserId); + + return envelope; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Infrastructure/Executor/EnvelopeReceiverExecutor.cs b/EnvelopeGenerator.Infrastructure/Executor/EnvelopeReceiverExecutor.cs new file mode 100644 index 00000000..ddcd9d28 --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/EnvelopeReceiverExecutor.cs @@ -0,0 +1,35 @@ +using Dapper; +using EnvelopeGenerator.Application.Contracts.Repositories; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using EnvelopeGenerator.Application.SQL; +using EnvelopeGenerator.Domain.Entities; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Infrastructure.Executor; + +public class EnvelopeReceiverExecutor: SQLExecutor, IEnvelopeReceiverExecutor +{ + private readonly IEnvelopeReceiverRepository _erRepository; + + public EnvelopeReceiverExecutor(IServiceProvider provider, IOptions sqlExecutorParamsOptions, IEnvelopeReceiverRepository erRepository) : base(provider, sqlExecutorParamsOptions) + { + _erRepository = erRepository; + } + + public async Task AddEnvelopeReceiverAsync(string envelope_uuid, string emailAddress, string? salutation, string? phone = null, CancellationToken cancellation = default) + { + using var connection = new SqlConnection(Params.ConnectionString); + var sql = Provider.GetRequiredService(); + var formattedSql = string.Format(sql.Raw, envelope_uuid.ToSqlParam(), emailAddress.ToSqlParam(), salutation.ToSqlParam(), phone.ToSqlParam()); + await connection.OpenAsync(cancellation); + var envelopeReceivers = await connection.QueryAsync(formattedSql); + var er = envelopeReceivers.FirstOrDefault(); + + if (er is null) + return null; + + return await _erRepository.ReadByIdAsync(envelopeId: er.EnvelopeId, receiverId: er.ReceiverId); + } +} diff --git a/EnvelopeGenerator.Infrastructure/Executor/Query.cs b/EnvelopeGenerator.Infrastructure/Executor/Query.cs new file mode 100644 index 00000000..5664d0be --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/Query.cs @@ -0,0 +1,41 @@ +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using Microsoft.EntityFrameworkCore; + +namespace EnvelopeGenerator.Infrastructure.Executor; + +public sealed record Query : IQuery +{ + private readonly IQueryable _query; + + internal Query(IQueryable queryable) + { + _query = queryable; + } + + public TEntity First() => _query.First(); + + public Task FirstAsync() => _query.FirstAsync(); + + public TEntity? FirstOrDefault() => _query.FirstOrDefault(); + + + public Task FirstOrDefaultAsync() => _query.FirstOrDefaultAsync(); + + + public TEntity Single() => _query.Single(); + + + public Task SingleAsync() => _query.SingleAsync(); + + + public TEntity? SingleOrDefault() => _query.SingleOrDefault(); + + + public Task SingleOrDefaultAsync() => _query.SingleOrDefaultAsync(); + + + public IEnumerable ToList() => _query.ToList(); + + + public async Task> ToListAsync() => await _query.ToListAsync(); +} \ No newline at end of file diff --git a/EnvelopeGenerator.Infrastructure/Executor/QueryExtension.cs b/EnvelopeGenerator.Infrastructure/Executor/QueryExtension.cs new file mode 100644 index 00000000..58c72850 --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/QueryExtension.cs @@ -0,0 +1,9 @@ +namespace EnvelopeGenerator.Infrastructure.Executor; + +public static class QueryExtension +{ + public static Query ToQuery(this IQueryable queryable) where TEntity : class + { + return new Query(queryable); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Infrastructure/Executor/SQLExecutor.cs b/EnvelopeGenerator.Infrastructure/Executor/SQLExecutor.cs new file mode 100644 index 00000000..5b8a0a3f --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/SQLExecutor.cs @@ -0,0 +1,33 @@ +using Dapper; +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Infrastructure.Executor; + +public class SQLExecutor : ISQLExecutor +{ + protected readonly SQLExecutorParams Params; + + protected readonly IServiceProvider Provider; + + public SQLExecutor(IServiceProvider provider, IOptions sqlExecutorParamsOptions) + { + Provider = provider; + Params = sqlExecutorParamsOptions.Value; + } + + public async Task> Execute(string sql, DynamicParameters parameters, CancellationToken cancellation = default) + { + using var connection = new SqlConnection(Params.ConnectionString); + await connection.OpenAsync(cancellation); + return await connection.QueryAsync(sql, parameters); + } + + public Task> Execute(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL + { + var sql = Provider.GetRequiredService(); + return Execute(sql.Raw, parameters, cancellation); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Infrastructure/Executor/SQLExecutorBaseEntity.cs b/EnvelopeGenerator.Infrastructure/Executor/SQLExecutorBaseEntity.cs new file mode 100644 index 00000000..d3e8b6d3 --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/SQLExecutorBaseEntity.cs @@ -0,0 +1,31 @@ +using EnvelopeGenerator.Application.Contracts.SQLExecutor; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Infrastructure.Executor; + +public sealed class SQLExecutor : SQLExecutor, ISQLExecutor where T : class +{ + private readonly EGDbContext _context; + + private readonly IServiceProvider _provider; + + public SQLExecutor(EGDbContext context, IServiceProvider provider, IOptions options) : base(provider, options) + { + _context = context; + _provider = provider; + } + + public IQuery Execute(string sql, CancellationToken cancellation = default, params object[] parameters) + => _context + .Set() + .FromSqlRaw(sql, parameters) + .ToQuery(); + + public IQuery Execute(CancellationToken cancellation = default, params object[] parameters) where TSQL : ISQL + { + var sql = _provider.GetRequiredService(); + return Execute(sql.Raw); + } +} diff --git a/EnvelopeGenerator.Infrastructure/Executor/SQLExecutorParams.cs b/EnvelopeGenerator.Infrastructure/Executor/SQLExecutorParams.cs new file mode 100644 index 00000000..734d4a5e --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/Executor/SQLExecutorParams.cs @@ -0,0 +1,6 @@ +namespace EnvelopeGenerator.Infrastructure.Executor; + +public class SQLExecutorParams +{ + public string? ConnectionString { get; set; } +} diff --git a/EnvelopeGenerator.Infrastructure/Repositories/EnvelopeHistoryRepository.cs b/EnvelopeGenerator.Infrastructure/Repositories/EnvelopeHistoryRepository.cs index 18ccc27e..cfa8313a 100644 --- a/EnvelopeGenerator.Infrastructure/Repositories/EnvelopeHistoryRepository.cs +++ b/EnvelopeGenerator.Infrastructure/Repositories/EnvelopeHistoryRepository.cs @@ -13,7 +13,7 @@ public class EnvelopeHistoryRepository : CRUDRepository By(int? envelopeId = null, string? userReference = null, int? status = null, bool withSender = false, bool withReceiver = false) { - var query = _dbSet.AsQueryable(); + var query = _dbSet.AsNoTracking(); if (envelopeId is not null) query = query.Where(eh => eh.EnvelopeId == envelopeId); diff --git a/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs b/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs index 8d5f471b..279d7f77 100644 --- a/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs +++ b/EnvelopeGenerator.Infrastructure/Repositories/EnvlopeReceiverRepository.cs @@ -48,9 +48,19 @@ public class EnvelopeReceiverRepository : CRUDRepository CountAsync(string uuid, string signature) => await ReadWhere(uuid: uuid, signature: signature).CountAsync(); - private IQueryable ReadById(int envelopeId, int receiverId, bool readOnly = true) + private IQueryable ReadById(int envelopeId, int receiverId, bool withEnvelope = true, bool withReceiver = true, bool readOnly = true) { var query = readOnly ? _dbSet.AsNoTracking() : _dbSet; + + if (withEnvelope) + query = query + .Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements!.Where(e => e.Receiver!.Id == receiverId)) + .Include(er => er.Envelope).ThenInclude(e => e!.History) + .Include(er => er.Envelope).ThenInclude(e => e!.User); + + if (withReceiver) + query = query.Include(er => er.Receiver); + return query.Where(er => er.EnvelopeId == envelopeId && er.ReceiverId == receiverId); } diff --git a/EnvelopeGenerator.Terminal/DependencyInjection.cs b/EnvelopeGenerator.Terminal/DependencyInjection.cs index ad011146..421a57d2 100644 --- a/EnvelopeGenerator.Terminal/DependencyInjection.cs +++ b/EnvelopeGenerator.Terminal/DependencyInjection.cs @@ -27,11 +27,11 @@ public static class DependencyInjection }); // Add envelope generator services - services.AddEnvelopeGeneratorRepositories(options => options.UseSqlServer(connStr)); + services.AddEnvelopeGeneratorInfrastructureServices(options => options.UseSqlServer(connStr)); return services .AddSingleton() - .AddEnvelopeGeneratorRepositories() + .AddEnvelopeGeneratorInfrastructureServices() .AddEnvelopeGeneratorServices(configuration) .AddSingleton(sp => { diff --git a/EnvelopeGenerator.Tests.Application/Mock.cs b/EnvelopeGenerator.Tests.Application/Mock.cs index 10bc7ea6..b14108e8 100644 --- a/EnvelopeGenerator.Tests.Application/Mock.cs +++ b/EnvelopeGenerator.Tests.Application/Mock.cs @@ -17,7 +17,7 @@ public class Mock builder.Configuration.AddJsonFile(configPath, optional: true, reloadOnChange: true); builder.Services - .AddEnvelopeGeneratorRepositories(opt => + .AddEnvelopeGeneratorInfrastructureServices(opt => { if (useRealDb) { diff --git a/EnvelopeGenerator.Web/Program.cs b/EnvelopeGenerator.Web/Program.cs index 9448b5e4..08e71c8a 100644 --- a/EnvelopeGenerator.Web/Program.cs +++ b/EnvelopeGenerator.Web/Program.cs @@ -16,6 +16,7 @@ using EnvelopeGenerator.Infrastructure; using EnvelopeGenerator.Web.Sanitizers; using EnvelopeGenerator.Application.Contracts.Services; using EnvelopeGenerator.Web.Models.Annotation; +using DigitalData.UserManager.DependencyInjection; var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); logger.Info("Logging initialized!"); @@ -90,7 +91,7 @@ try }); // Add envelope generator services - builder.Services.AddEnvelopeGeneratorRepositories(options => options.UseSqlServer(connStr)); + builder.Services.AddEnvelopeGeneratorInfrastructureServices(options => options.UseSqlServer(connStr)); builder.Services.AddEnvelopeGeneratorServices(config); @@ -168,6 +169,8 @@ try builder.ConfigureBySection(); + builder.Services.AddUserManager(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/EnvelopeGenerator.Web/appsettings.json b/EnvelopeGenerator.Web/appsettings.json index 1c6fb8c4..6c058cd7 100644 --- a/EnvelopeGenerator.Web/appsettings.json +++ b/EnvelopeGenerator.Web/appsettings.json @@ -140,14 +140,15 @@ } }, "TFARegParams": { - "TimeLimit": "00:30:00" + "TimeLimit": "90.00:00:00" }, "DbTriggerParams": { "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], "EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], "EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ], "EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ], - "Receiver": [] + "Receiver": [], + "EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ] }, "MainPageTitle": null, "AnnotationParams": {